Upload a Post
Create a post and fan it out to one or more platforms in a single call.
/v1/uploadBody parameters
| Field | Type | Required | Description |
|---|---|---|---|
| caption | string | Yes | Post text. Platforms with a shorter character cap (e.g. Threads at 500 characters) fail that target with caption_too_long rather than truncating. |
| media | array<{ url, type }> | No | Publicly reachable HTTPS media URLs. type is "image" or "video". Omit entirely for a text-only post (Threads and X only — Instagram and TikTok require media). |
| account_ids | array<string> | One of account_ids / platforms | One target is created per id, e.g. ["acct_ig_5f8b2c9a1d7e4f30", "acct_ig_9c1a7e2b8f4d4056"]. This is the only way to target more than one connected profile on the same platform in a single call (e.g. two Instagram accounts at once) — get ids from GET /v1/accounts. Preferred over platforms for anything beyond the simplest one-account-per-platform case. |
| platforms | array<"instagram"|"threads"|"tiktok"|"x"> | One of account_ids / platforms | Legacy convenience: one target per platform, auto-resolved to the workspace's single connected account for it. Fails with 422 account_not_connected if zero or more than one account is connected for a listed platform — use account_ids or the accounts field below to disambiguate instead. |
| accounts | object<platform, account_id> | No | Only used together with platforms: which connected account to use per platform, e.g. { "instagram": "acct_ig_5f8b2c9a1d7e4f30" }. Still only one account per platform — use account_ids to target several accounts on the same platform. |
| sandbox | boolean | No | Inferred from the API key prefix if omitted. Must agree with the key's own mode — see Sandbox Mode. |
| simulate_error | string | No | Sandbox only. Forces every target to fail with the given error code. See Errors. |
Request
This example publishes to two Instagram accounts and one Threads account in a single call — only possible with account_ids, since platforms only ever creates one target per platform.
curl -X POST https://api.omnipost.dev/v1/upload \
-H "Authorization: Bearer $OMNIPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"caption": "New drop is live 🚀",
"media": [
{ "url": "https://cdn.yourapp.com/campaign/hero.jpg", "type": "image" }
],
"account_ids": [
"acct_ig_5f8b2c9a1d7e4f30",
"acct_ig_9c1a7e2b8f4d4056",
"acct_th_2b7e4f309c1a7e2b"
]
}'Response
201 Created — the post was accepted and targets are queued. Publishing itself is asynchronous; poll or use a webhook to learn the outcome.
{
"id": "post_8f3d1a9c2b7e4f01",
"status": "pending",
"sandbox": false,
"caption": "New drop is live 🚀",
"media": [
{ "url": "https://cdn.yourapp.com/campaign/hero.jpg", "type": "image" }
],
"targets": [
{ "platform": "instagram", "status": "pending", "external_url": null, "error_code": null, "error_message": null, "published_at": null },
{ "platform": "instagram", "status": "pending", "external_url": null, "error_code": null, "error_message": null, "published_at": null },
{ "platform": "threads", "status": "pending", "external_url": null, "error_code": null, "error_message": null, "published_at": null }
],
"created_at": "2026-08-02T00:00:00.000Z",
"scheduled_at": null
}Media constraints
Each platform has its own accepted formats, size, and duration limits — see the platform guides for the full breakdown. Media that a given platform rejects fails only that target with 422 unsupported_media_for_platform (as the immediate response, if detected during validation) or an unsupported_media_for_platform target error (if only discoverable after upload) — the rest of the targets are unaffected.
Media must be URL-reachable
OmniPost fetches your media from the URL you provide — it doesn't accept raw multipart file uploads. Host media somewhere publicly reachable over HTTPS (your CDN, S3/R2 with a public read policy, etc.) before calling this endpoint.