REST API

Errors

One shape, specific codes, and values you can act on.

The shape

Failures come back as an error object with a code and an English message. Anything the message interpolated is also sent as its own field, so you can rebuild the sentence in your own language.

We do not accept an Idempotency-Key header yet. Duplicate publishes are caught by content fingerprint instead: the same text and files to the same account within ten minutes is refused with duplicate_post. Pass force: true when you mean it.

Several violations come back together

When more than one rule is broken, error.errors carries all of them and the first one is also spread into error itself. Fix them in one pass instead of learning about the next one after each retry. Two exceptions come alone because everything else depends on them: no_accounts and empty_content.

{
  "error": {
    "code": "content_too_long",
    "message": "Threads allows 500 characters. This is 620.",
    "platform": "threads",
    "limit": 500,
    "actual": 620
  }
}

Codes

NameTypeDescription
verification_required401Email and phone verification are not finished.
invalid_api_key401The bearer token is not a valid key.
forbidden_scope403The key lacks the scope. Sends scope.
no_accounts422accountIds was empty. Sends candidates, and next when nothing is connected. Sends needsReconnect and a reconnect hint when the channels you have are expired rather than missing.
empty_content422Neither text nor media was given.
account_not_connected404One of the accountIds is not connected. Sends candidates.
account_expired422The channel token expired. Sends platform.
platform_not_available422That channel is not live yet. Sends platform.
content_too_long422Sends platform, limit and actual.
media_required422That channel cannot post text alone. Sends platform.
media_mixed422Images and video in one post. Sends platform.
media_too_many422Sends platform, limit and actual.
media_too_large422Sends limit, limitMb and actual.
media_aspect_ratio422Sends limit and actual.
media_dimensions422Video wider than the limit. Sends limit and actual. Image width is never a reason.
media_type_not_supported422Sends types.
media_too_long422Video longer than the limit. Sends limit and actual.
media_url_invalid422Not an https URL.
media_url_blocked422That address is private, loopback or link-local.
media_url_unreachable422The host did not answer, or redirected too many times.
workspace_required400This account has more than one workspace and the request did not say which. Sends workspaces, each with id, name, slug and channels, so you can match what the person said without extra calls. Pass the x-uplika-workspace header, or the workspaceId argument on MCP tools.
workspace_mismatch404The post, channel, or media is in a different workspace on the same account. Sends workspaces. Retry there instead of connecting the account again.
media_not_uploaded404media_complete was never called.
media_expired404The upload was reclaimed. Upload the file again.
media_not_configured503Media storage is not set up.
post_not_found404No such publish in this workspace.
media_not_found404No such media in this workspace.
key_not_found404No such API key.
grant_not_found404No such authorization.
nothing_to_retry422Every target already succeeded.
not_published422The post has not gone out yet.
platform_error502The channel refused or did not answer.
duplicate_post409The same content went to this account within 10 minutes. Sends postId. Pass force: true to publish anyway.
unauthenticated401No session and no valid key.
platform_rejected_content400 · 404 · 422 · 502The channel refused the content itself. Retrying will not help. Read paths pass the upstream status through; the publish path reports it as 502.
platform_auth_error502The channel refused our access to that account. Reconnect it. Kept at 502 so it is never confused with a problem with your uplika key.
topic_tag_invalid422The topic tag has a period or an ampersand in it. Threads does not allow those.
topic_tag_not_supported422One of the channels you picked does not take a topic tag. Sends platform. Publish to it separately or drop the tag.
api_key_revoked401That key was revoked. Create a new one. Kept separate from invalid_api_key so you do not go looking for a typo.
api_key_expired401That key passed its expiry. Create a new one.
idempotency_key_reused422That Idempotency-Key was already used with a different body. Use a new key for a new request.
publish_in_flight409That post is still going out. Deleting mid-flight would let it publish after you deleted it. Wait a few seconds and try again.
thread_and_content422You passed both content and threadItems. Pass one.
thread_empty422threadItems was an empty array.
container_expired502The upload container expired before the post went out. Create the post again.
permission_denied502The channel refused for permission reasons.
internal_error500Our fault. The response carries no detail; the request id is in the logs.
rate_limited429Over the per-minute limit. Sends limit and retryAfter, and the response carries Retry-After.
account_not_found404No such connected account in this workspace.
empty_reply422The reply had no text.
media_fields_required422presign needs fileName, contentType and bytes.
not_our_post422That post was imported from the channel, not published through us.
key_name_required422Give the key a name.
invalid_locale422locale must be en or ko.
bad_post_ref422The post reference was not an id, a media id or a post link.
post_not_owned422That post belongs to an account this workspace has not connected.
post_not_in_recent422That post was not in the recent window we searched.
upload_session_not_found404That upload link is gone or expired.
upload_session_full422That upload link already has its maximum files.
media_url_invalid422Not an https URL.
media_url_blocked422That address is private, loopback or link-local.
media_url_unreachable422The host did not answer, or redirected too many times.
workspace_required400This account has more than one workspace and the request did not say which. Sends workspaces, each with id, name, slug and channels, so you can match what the person said without extra calls. Pass the x-uplika-workspace header, or the workspaceId argument on MCP tools.
workspace_mismatch404The post, channel, or media is in a different workspace on the same account. Sends workspaces. Retry there instead of connecting the account again.
youtube_title_required422A YouTube target needs options.youtube.title.
youtube_title_too_long422The title is over 100 characters. Sends limit and actual.
youtube_angle_brackets422YouTube refuses < and > in the title and the description.
youtube_video_required422YouTube takes exactly one video. Sends limit and actual.
youtube_images_not_supported422YouTube does not take images in a post. Use options.youtube.thumbnailMediaId instead. Sends actual.
youtube_tags_too_long422Tags are over 500 characters in total. A tag containing a space costs two extra for the quotes YouTube adds. Sends limit and actual.
youtube_schedule_needs_private422options.youtube.publishAt only works while privacyStatus is private.
youtube_schedule_invalid422options.youtube.publishAt was not an ISO 8601 timestamp.
youtube_thumbnail_type422A thumbnail must be image/jpeg or image/png.
youtube_thumbnail_too_large422A thumbnail can be up to 2MB. Sends limit and actual.
youtube_short_no_thumbnail422A vertical video of three minutes or less publishes as a Short, and Shorts do not take custom thumbnails. Drop the thumbnail or change the video.
youtube_no_threads422threadItems with a YouTube account would upload one video per item. Publish the video on its own.
beta_access_required403That channel is in beta and this workspace has not been approved for it yet. Request access from the dashboard. Retrying will not help.

How do I see what my key actually sent?

Every REST and MCP call is logged with its request and response body. This is the fastest way to find out why a publish was refused, because the channel's own words are in there.

GET/v1/logs

Recent API and agent calls, newest first, with bodies.

Paging

NameTypeDescription
requestIdqueryOne call by the id an error response gave you.
outcomequeryfailed for 4xx and 5xx, ok for the rest. Omit for everything.
searchqueryMatches the path or the request id.
sincequeryYYYY-MM-DD. Calls from that day onward.
limitquery1-200, defaults to 50.
beforequeryA log id from a previous page's nextBefore.

Common questions

What shape does an error come back in?

An error object with a code and an English message. Whatever the message interpolated is also sent as its own field, so you can rebuild the sentence in your own language instead of parsing ours.

Why are the messages English only?

Because a response that changed language would answer the same request differently depending on who asked. The screen is bilingual; the API is not. Render from code, not from message.

What should I do when I get rate_limited?

Wait and call again. The response carries the limit that was hit, so a client can pace itself rather than guessing at a backoff.