dopost API

One REST call posts text, images, carousels or a video to every connected account of a workspace, in the format each platform takes. Base URL: https://dopost.app/v1. JSON in, JSON out.

Quick start

  1. Sign up, connect your accounts on the Channels page of a workspace.
  2. Create an API key on the API page of that workspace (every plan; Free has one key).
  3. List the accounts, then post:
curl https://dopost.app/v1/accounts -H "Authorization: Bearer dp_live_..."

curl https://dopost.app/v1/posts \
  -H "Authorization: Bearer dp_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1234" \
  -d '{
    "text": "New in the shop today #sale",
    "media": ["https://example.com/photo-1.jpg", "https://example.com/photo-2.jpg"],
    "accounts": ["acc_facebook_id", "acc_instagram_id"],
    "link": "https://shop.example.com/sale"
  }'

Authentication

Send the key as Authorization: Bearer dp_live_…. A key belongs to one workspace and is shown once when created; revoke it on the API page. Keys are stored as a hash: we cannot show them again.

Rate limits

Per key and per minute. Over the limit you get 429 with Retry-After. On the Free plan, creating a post (POST /v1/posts) and uploading media (POST /v1/media) have their own, lower limits.

PlanRequests / minAccountsPosts / monthWebhooksConnect links
free20 (new posts 1, media uploads 5)160nono
lite603100nono
creator30019unlimited*yesno
business30049unlimited*yesyes
agency1000119unlimited*yesyes

* Unlimited plans: at most 25 posts per account per day and 5,000 a month. A post to 3 accounts counts 3 posts.

Accounts

GET/v1/accounts

The workspace's connected accounts: id, platform, name, username, status (active | expired | revoked | error), externalRef, capabilities.

DELETE/v1/accounts/{id}

Disconnects an account (its tokens are deleted).

Connect links

Business plan and up. Let your own users connect their accounts to your workspace without a dopost account.

POST/v1/accounts/connect-link
{ "externalRef": "user_42", "redirectUrl": "https://yourapp.com/connected", "platforms": ["instagram", "tiktok"] }
→ 201 { "url": "https://dopost.app/connect/…", "expiresAt": "…" }

Send your user to url (valid 24 hours). When done they come back to redirectUrl?status=connected&platform=…&accounts=id1,id2 (or status=error&error=CODE). The new accounts carry your externalRef.

Media

Images (JPG, PNG, WebP, GIF, HEIC; up to 20 MB) and videos (MP4, MOV, WebM in H.264 / HEVC, 1 s – 60 min; size by plan). Files are kept 30 days and then deleted: a post cannot be scheduled after its media expires. The simplest way is to pass URLs in media of a post. To upload yourself:

POST/v1/media
{ "url": "https://example.com/video.mp4" }            → 201 { id, status: "ready", url, … }
{ "contentType": "video/mp4", "size": 48213456 }    → 201 { id, status: "pending", uploadUrl, … }

With uploadUrl: PUT the file there with the same Content-Type, then call POST /v1/media/{id}/complete.

GET/v1/media/{id}

An asset: kind, status, url, thumbnailUrl, width, height, durationMs, expiresAt.

Posts

POST/v1/posts
{
  "text": "Caption with #hashtags",
  "media": ["med_id", "https://example.com/image.jpg"],   // ids or URLs, in order
  "accounts": ["acc_1", "acc_2"],
  "title": "Video title",                                  // YouTube (required), TikTok photo posts
  "firstComment": "First comment",                         // Facebook, Threads, Bluesky
  "link": "https://…",                                     // first comment on Facebook / Threads, pin link on Pinterest,
                                                           // button URL on Google Business, end of the text elsewhere
  "scheduleAt": "2026-12-01T09:00:00+07:00",               // omit to post now
  "queue": true,                                           // or: the workspace's next free posting time
  "draft": true,                                           // or: save without posting
  "overrides": {
    "acc_2": {
      "text": "Shorter text for this account",
      "media": ["med_other"],
      "youtube": { "privacy": "unlisted" },
      "format": "story",                                   // Instagram / Facebook: a story (one image or video)
      "boardId": "123",                                    // Pinterest (the account's first board by default)
      "cta": "BOOK",                                       // Google Business: LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL
      "tiktok": { "privacyLevel": "PUBLIC_TO_EVERYONE", "allowComment": true, "allowDuet": false, "allowStitch": false, "brandOrganic": false, "brandContent": false }
    }
  }
}

201 with the post. Each account's result is in targets[]: status (scheduled → processing → posted | failed), url, error. Send an Idempotency-Key header to make retries safe: the same key returns the first post (200, Idempotent-Replayed: true). TikTok posts need overrides[id].tiktok.privacyLevel.

POST/v1/posts/validate

Same body, nothing is posted: { valid, targets: [{ accountId, platform, issues: [{ code, values }] }] }.

GET/v1/posts?status=&limit=&cursor=

Newest first, cursor pagination (nextCursor). Status: draft, scheduled, publishing, done, partial, failed, canceled.

GET/v1/posts/{id}

One post with its targets. A posted target has metrics once its counts were read ({ views, likes, comments, shares, updatedAt }, null where the platform has no such count): Facebook, Instagram, Threads, YouTube and Bluesky, refreshed for 30 days after posting.

PATCH/v1/posts/{id}

{ scheduleAt } moves a scheduled post.

DELETE/v1/posts/{id}

Cancels the accounts not posted yet (their posts are given back to your monthly count).

Webhooks

Creator plan and up. Add endpoints on the API page or with the API; the signing secret is shown once.

POST/v1/webhooks
{ "url": "https://yourapp.com/hooks/dopost", "events": ["post.published", "post.failed", "account.expired", "account.disconnected"] }
→ 201 { id, url, events, secret: "whsec_…" }

Each delivery is a POST with X-Dopost-Event and X-Dopost-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your secret. Answer 2xx; failed deliveries are retried with backoff for about a day.

import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return Math.abs(Date.now() / 1000 - Number(t)) < 300 && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
{ "id": "evt_…", "type": "post.published", "createdAt": "…",
  "data": { "postId": "…", "targetId": "…", "account": { "id": "…", "platform": "instagram", "name": "…", "externalRef": null },
            "status": "posted", "url": "https://www.instagram.com/p/…", "error": null } }

Errors

{ "error": { "code": "VALIDATION_FAILED", "message": "…", …details } } with 400 (input), 401 (key), 402 (POST_LIMIT, CHANNEL_DAY_LIMIT), 403 (plan), 404, 409, 422 (VALIDATION_FAILED with targets), 429.

SDKs and n8n

@dopost/sdk: typed, no dependencies, Node 18+. It retries 429 / 5xx answers where that is safe and verifies webhook signatures. Source: sdk/node.

import { Dopost, verifyWebhook } from "@dopost/sdk";

const dopost = new Dopost({ apiKey: process.env.DOPOST_API_KEY });
const accounts = await dopost.accounts.list();
const post = await dopost.posts.create(
  { text: "Hello from the SDK", media: ["https://example.com/photo.jpg"], accounts: accounts.map((a) => a.id), queue: true },
  { idempotencyKey: "hello-1" }
);

Python: pip install dopost (standard library only, source sdk/python). n8n: the community node n8n-nodes-dopost (Settings → Community Nodes), source sdk/n8n.

from dopost import Dopost

dopost = Dopost(api_key="dp_live_...")
ids = [a["id"] for a in dopost.accounts.list()]
dopost.posts.create(ids, "Hello from Python", media=["https://example.com/photo.jpg"], queue=True)

MCP server (AI agents)

Claude, ChatGPT and other MCP clients can post through dopost. Add a remote MCP server with the URL https://dopost.app/mcp (Streamable HTTP) and the header Authorization: Bearer dp_live_…. Tools: list_accounts, import_media, validate_post, create_post, get_post, list_posts, cancel_post. They run the REST API with your key, so the same plan limits, checks and quotas apply.

claude mcp add --transport http dopost https://dopost.app/mcp --header "Authorization: Bearer dp_live_..."

What each platform takes

PlatformText onlyImagesVideosMixedText limitFirst comment
YouTubenono1 · 720 minno5000no
TikToknoup to 351 · 10 minno2200no
Facebookyesup to 101 · 240 minno63206yes
Instagramnoup to 1010 · 15 minyes2200no
Threadsyesup to 2020 · 5 minyes500yes
LinkedInyesup to 201 · 30 minno3000no
Telegramyesup to 1010 · 720 minyes4096 (1024 with media)no
Discordyesup to 1010 · 720 minyes2000no
Blueskyyesup to 41 · 3 minno300yes
Pinterestnoup to 51 · 15 minno500no
Google Businessyesup to 1nono1500no

Instagram takes images between 4:5 and 1.91:1 (PNG is converted to JPEG). TikTok posts stay private ("only me") until our TikTok app review is done. YouTube posts count against a shared daily upload quota.