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
- Sign up, connect your accounts on the Channels page of a workspace.
- Create an API key on the API page of that workspace (every plan; Free has one key).
- 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.
| Plan | Requests / min | Accounts | Posts / month | Webhooks | Connect links |
|---|---|---|---|---|---|
| free | 20 (new posts 1, media uploads 5) | 1 | 60 | no | no |
| lite | 60 | 3 | 100 | no | no |
| creator | 300 | 19 | unlimited* | yes | no |
| business | 300 | 49 | unlimited* | yes | yes |
| agency | 1000 | 119 | unlimited* | yes | yes |
* Unlimited plans: at most 25 posts per account per day and 5,000 a month. A post to 3 accounts counts 3 posts.
Accounts
The workspace's connected accounts: id, platform, name, username, status (active | expired | revoked | error), externalRef, capabilities.
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.
{ "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:
{ "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.
An asset: kind, status, url, thumbnailUrl, width, height, durationMs, expiresAt.
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.
Same body, nothing is posted: { valid, targets: [{ accountId, platform, issues: [{ code, values }] }] }.
Newest first, cursor pagination (nextCursor). Status: draft, scheduled, publishing, done, partial, failed, canceled.
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.
{ scheduleAt } moves a scheduled post.
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.
{ "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
| Platform | Text only | Images | Videos | Mixed | Text limit | First comment |
|---|---|---|---|---|---|---|
| YouTube | no | no | 1 · 720 min | no | 5000 | no |
| TikTok | no | up to 35 | 1 · 10 min | no | 2200 | no |
| yes | up to 10 | 1 · 240 min | no | 63206 | yes | |
| no | up to 10 | 10 · 15 min | yes | 2200 | no | |
| Threads | yes | up to 20 | 20 · 5 min | yes | 500 | yes |
| yes | up to 20 | 1 · 30 min | no | 3000 | no | |
| Telegram | yes | up to 10 | 10 · 720 min | yes | 4096 (1024 with media) | no |
| Discord | yes | up to 10 | 10 · 720 min | yes | 2000 | no |
| Bluesky | yes | up to 4 | 1 · 3 min | no | 300 | yes |
| no | up to 5 | 1 · 15 min | no | 500 | no | |
| Google Business | yes | up to 1 | no | no | 1500 | no |
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.