Start
Errors
The one error shape, every code in the catalogue, and what to do about each.
The shape
Every error the public API raises itself has one body. code is stable for the life of v1 and is the thing to switch on; message is for a person; field names the offending input when there is one.
{
"error": {
"code": "SCHEDULED_TIME_IN_PAST",
"message": "scheduledAt is 42 minutes in the past. Send a time in the future, or omit it to create a draft.",
"field": "scheduledAt"
}
}Three families do not use this shape. Authentication and rate-limit failures answer with the flat body shown on Authentication and Rate limits. Schema validation of a request body (an unknown field, a wrong type, a missing required field) answers 400 with the framework's flat body: { "statusCode": 400, "message": ["..."], "error": "Bad Request" }. Unknown fields are refused, never ignored. And a post that breaks one target channel's limits on create or edit is refused by the shared publish checks, which answer 400 with a flat body carrying the code at the top level: { "statusCode": 400, "code": "TOO_LONG", "message": "X posts are limited to 280 characters (this one is 301)." }. Those codes are listed after the catalogue. Every flat body also carries error, path and timestamp beside the fields shown here; the examples on these pages list the fields worth switching on.
Warnings
Things that happened without failing the call ride on successful writes as warnings: [{ code, message }]. Two exist: APPROVAL_REQUIRED (the post is queued as pending) and SCHEDULED_TIME_COERCED (a scheduledAt up to five minutes in the past was moved to now).
The catalogue
| Code | Status | Meaning | Recovery |
|---|---|---|---|
MEDIA_REQUIRED | 400 | The post targets Instagram and carries no media. (YouTube, TikTok and Pinterest refuse a text-only scheduled post with the same code in the flat channel-limit body below.) | Add mediaUrls or mediaIds. |
MEDIA_NOT_FOUND | 404 | A mediaIds entry is not a media asset in this workspace. | Use the id returned by POST /media/upload-url. |
MEDIA_NOT_READY | 409 | The asset has not been completed, or the bytes are not in storage yet. | PUT the file, then POST /media/:id/complete. |
MEDIA_TOO_LARGE | 400 | Over the cap: 15 MB images, 500 MB video by upload, 25 MB video by URL. (A video over one target channel's own size cap carries the same code in the flat channel-limit body below.) | Shrink it, or use the presigned upload for large video. |
MEDIA_URL_REFUSED | 400 | The URL is not public https, did not answer 2xx, or did not return an image or video. | Host the file on a public https URL. |
MEDIA_STORAGE_UNAVAILABLE | 503 | Object storage is not configured or not reachable. | Retry later; nothing was written. |
SCHEDULED_TIME_IN_PAST | 400 | scheduledAt is more than five minutes in the past. | Send a future time, or omit it for a draft. |
PLATFORM_SETTING_UNKNOWN | 400 | A key in platformSettings is unknown, mistyped, or for a platform the post does not target. | The field names the path; check the platform page. |
POST_NOT_FOUND | 404 | No post with that id in this workspace. | List with GET /posts. |
POST_NOT_EDITABLE | 409 | The post is publishing, published, partially published or cancelled. | Create a new post. |
CONNECTION_NOT_FOUND | 404 | A connectionIds entry is not in this workspace. | List with GET /connections. |
CONNECTION_EXPIRED | 409 | The connection is not active or its token has expired. | Reconnect the account in the app. |
APPROVAL_REQUIRED | 409 | POST /posts/:id/publish on a post that is pending approval. | A member approves it in the app. Do not retry. |
IDEMPOTENCY_KEY_CONFLICT | 422 | The Idempotency-Key was used with a different request. | Use a fresh key for a new request. |
IDEMPOTENCY_IN_FLIGHT | 409 | A request with this key is still running. | Wait, then retry with the same key to get its result. |
SCOPE_MISSING | 403 | The key lacks the route's scope. | Create a key with the scope. |
VALIDATION_FAILED | 400 (409 for a taken slug) | An Idempotency-Key over 255 characters, an upload contentType the kind does not accept, a taken workspace slug, or a managed key asking only for workspaces:manage. | The field names it. |
WEBHOOK_NOT_FOUND | 404 | No endpoint with that id. | List with GET /webhooks. |
WEBHOOK_URL_REFUSED | 400 | Not https, or a localhost, private or link-local address, or credentials in the URL. | Use a public https URL. |
WEBHOOK_EVENT_UNKNOWN | 400 | An event name outside the catalogue. | See Webhooks for the list. |
WEBHOOK_DELIVERY_NOT_FOUND | 404 | No delivery with that id on that endpoint. | List with GET /webhooks/:id/deliveries. |
WORKSPACE_NOT_FOUND | 404 | No managed workspace with that id under this key's workspace. | List with GET /workspaces. |
WORKSPACE_NOT_MANAGED | 403 (400 when malformed) | x-overads-workspace names a workspace this key cannot act for, or is not a single uuid. | Create the workspace over POST /workspaces, or drop the header. |
WORKSPACE_HEADER_NOT_ALLOWED | 400 | x-overads-workspace was sent to a /workspaces/* route. | Drop the header; those routes act on the agency. |
CONNECT_TOKEN_INVALID | 404 | A hosted connect link that never existed or was rotated away. | Mint a new one with POST /workspaces/:id/connect-url. |
CONNECT_TOKEN_EXPIRED | 410 | A hosted connect link past its expiresAt. | Mint a new one. |
CONNECT_SESSION_INVALID | 401 | The 15-minute session behind a connect attempt is bad or its link is no longer live. | Reload the connect page. |
Channel-limit refusals
Every target of a post is checked against its channel's limits when the post is created or edited (POST /posts, PATCH /posts/:id, create_post, update_post). A failure answers 400 with the flat body { statusCode, code, message }; message names the channel and the cap, for example Bluesky takes either images or one video, not both in one post. Video size and duration are checked only when overads knows them from the file's media asset record; nothing is guessed from a URL.
| Code | Meaning | Recovery |
|---|---|---|
TOO_LONG | The copy for one channel is over its character cap. | Give that channel a shorter variants entry. |
TOO_MANY_ITEMS | More images, videos or total items than the channel takes; or a second video on a channel that takes one. | Trim the media list, or split the post. |
MEDIA_UNSUPPORTED | Images and a video together on Bluesky or TikTok, or images on YouTube, which takes video only. | Send one kind of media to that channel. |
MEDIA_REQUIRED | A scheduled post with no media for YouTube (one video), TikTok (one video or photos) or Pinterest (one image or one video). A draft is allowed to be incomplete. | Attach media before scheduling. |
MEDIA_TOO_LARGE | A video over the channel's file cap (Bluesky 100 MB, Mastodon 99 MB, X 512 MB, LinkedIn 500 MB, Instagram, Facebook and Threads 1 GB, Pinterest 2 GB, TikTok 4 GB). | Shrink it, or drop that channel. |
MEDIA_TOO_LONG | A video over the channel's duration cap (X 140 seconds, Bluesky 3 minutes, Threads 5, TikTok 10, Instagram 15, Pinterest 15, Facebook 20, LinkedIn 30, YouTube 12 hours). | Trim it, or drop that channel. |
MEDIA_TOO_SHORT | A Pinterest video under 4 seconds. | Use a longer video. |
BOARD_REQUIRED | A Pinterest post scheduled without platformSettings.pinterest.boardId. | Pass boardId; the app's channel settings list the account's boards. |
Per-account failures are not errors
A post to three accounts that lands on two answers 200. The account that failed is in targets with status: "failed" and the platform's error text verbatim in error. Read targets, not just the HTTP status, and subscribe to post.failed to be told.
