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"
}
}Two 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.
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. | 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, or over the target channel's own video size cap. | Shrink it, or use the presigned upload for large video. |
MEDIA_TOO_LONG | 400 | A video runs longer than the target channel allows (X 140 seconds, Threads 5 minutes, TikTok 10 minutes, Instagram 15, Facebook 20, LinkedIn 30). | Trim it, or drop that channel from connectionIds. |
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 | An Idempotency-Key over 255 characters, or a contentType the upload does not accept. | 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. |
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.