Guides
Errors & retries
Every error, at the request level or per target, uses one normalised shape, so you (or an agent) can handle failures the same way everywhere.
The error envelope
Errors return { "error": { … } } with a stable type, a human message, an optional platform, and a retryable boolean.
{
"error": {
"type": "entitlement_exceeded",
"message": "Not enough credits: this post costs 5, you have 2.",
"platform": null,
"retryable": false
}
}Two levels of error
- Request-level: the whole call is rejected, because nothing could proceed for any destination. An unknown media id, an unknown account id, no text, bad auth, out of credits. Returned with a
4xxstatus and the envelope above. No post is created. - Per-target: the request was fine, but a network will not take it (over a limit, missing media, invalid option). The call returns
201, the post is created, and the failingtargets[].errorcarries the same envelope. You're not charged for failed targets.
A per-target failure never changes the status code. You get 201 whether one target failed or all of them, and whether the post was immediate or scheduled. So read state and targets[], not just the status:
post state | What happened |
|---|---|
published | Every target went out. |
partial | Some targets published, some failed. Check each one. |
scheduled | At least one target is queued to fire. |
failed | Nothing published, and nothing is queued. On a scheduled post this means every target was refused, so it will never fire. |
When a network refuses
These come from the network rather than from us. We recognise the message, attach a code you can branch on, and say what actually fixes it. Anything we do not recognise is passed through unchanged, because a confident wrong instruction is worse than none.
| Code | Network | What to do |
|---|---|---|
account_unknown | Any | A connected-account id does not resolve, or is no longer connected. List them with GET /v1/social-accounts, or retrieve one with GET /v1/social-accounts/{id} to check it is still connected. |
aspect_ratio_unsupported | Instagram only accepts aspect ratios between 4:5 and 1.91:1. Crop the image and try again. | |
branded_content_not_enabled | Any | The Page has not turned on branded content partner allowlisting. In Meta Business Suite open the Page, go to branded content settings and turn on partner allowlisting. Reconnecting will not change it. |
branded_content_private | Any | A TikTok post is marked as a paid partnership and set to private at the same time. Branded content has to be visible to somebody. Pick PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS or FOLLOWER_OF_CREATOR, or drop the paid-partnership disclosure. TikTok's own composer greys the private option out for the same reason, and its API refuses the pair. |
channel_gone | Any | A target was accepted and the channel was disconnected before it published. This target failed and was not charged. Reconnect the channel and post again; the other targets on the post are unaffected. |
creator_marketplace_not_onboarded | Any | The brand has not accepted the creator marketplace terms. In Meta Business Suite, open Creator Marketplace, choose Get Started and accept the terms. Nothing in this API can do it: it is a one-off consent a person has to give. |
creator_marketplace_unavailable | Any | The network has not published this edge at all. Search creators through an Instagram account connected the Facebook way instead. Reconnecting or re-requesting the permission will not change it. |
creator_search_unsupported | Any | The network has no creator marketplace at all, so there is nothing to search. Check discovers.creators on GET /v1/social-accounts/{id} first. A false there is a fact about the network, not something a reconnect or a permission changes. |
duplicate_post | Facebook, X | Facebook refuses two identical posts in a row. Change the wording and post again. |
field_unknown | Any | The request carried a field we do not recognise, usually a typo. Remove it or fix the spelling. A mistyped field would otherwise be ignored silently, which is how a setting you thought you sent goes missing. |
field_wrong_type | Any | A field was sent as the wrong shape, e.g. a string where an array was expected. `param` names it. Check the field named in `param` against the schema at GET /openapi.json. |
handle_not_a_username | Any | A numeric id was passed where a username was expected. Instagram looks people up by username only. If the id came from a hashtag result, stop: Meta strips the author from those and publishes no way back from a media id or permalink to the person. Creator search is the only route from a post-shaped thing to a contactable account. |
handle_not_found | Any | The network has no account with that handle. Check the spelling. Instagram only answers for professional (business or creator) accounts, so a personal handle comes back as not found. A media or post id is not a handle: Meta publishes no way to get from a post to its author, so a hashtag result cannot be turned into one. |
hashtag_only_search | Any | A multi-word query was sent to a network that only searches hashtags. Instagram has no keyword search. Search a single hashtag with no spaces. Networks with real keyword search (Threads, Bluesky) accept the phrase as-is. |
image_too_large | TikTok | TikTok rejected an image's dimensions after upload. Images must be between 540x960 and 1080x1920. |
instagram_facebook_door_required | Any | The feature exists only on an Instagram connected through Facebook, and this one was connected with Instagram Login. Reconnect through Facebook: GET /v1/connect-link with platform=instagram and variant=facebook. Creator search, branded content and messages exist only on that door, and no permission change reaches them from the other one. Check connectVariant on GET /v1/social-accounts/{id} before calling. |
instagram_messages_switched_off | This Instagram account has message access switched off, which is separate from the permission. In the Instagram app open Settings, then Messages and story replies, then Connected tools, and turn on Allow access to messages. Reconnecting will not fix it. | |
instagram_reply_window_closed | Instagram only allows a reply within 24 hours of the person's last message, and that window has closed. A reply a person writes can use humanAgent: true for 7 days; otherwise answer from the Instagram app. | |
invalid_return_url | Any | The connect-link returnUrl is missing, too long, not a URL, or not https (except localhost). Send an absolute https URL, e.g. https://yourapp.com/social-connected. http is allowed only for localhost during development. |
invalid_webhook_events | Any | Webhook events were not sent as an array. Send an array of supported event names. |
invalid_webhook_url | Any | The webhook URL cannot be parsed as a complete URL. Provide a complete HTTPS URL. |
link_unreachable | Facebook could not read the link in this post. Check the URL opens publicly, then try again. | |
linkedin_video_unsupported | LinkedIn does not accept video posts to a personal profile through the API. Post it as an image or text, or use a Company Page. | |
media_required | YouTube | YouTube needs the video file itself. Attach a video and try again. |
media_too_large | Bluesky | Bluesky caps images at roughly 1MB. Compress the image and try again. |
media_type_unsupported | Instagram, X | Instagram rejected this file. Use JPEG for images, or MP4 with H.264 video and AAC audio. |
media_unknown | Any | A media id in the request does not resolve. Upload with POST /v1/media and use the med_… id it returns. A media id is scoped to your account, so one from somewhere else will not resolve. |
media_unreadable | Pinterest could not read the image. Make sure it is a public JPEG or PNG under 20MB. | |
meta_identity_check | Facebook wants this Page's identity confirmed before it will publish. Open Meta Business Suite, finish the prompt on the Page, then post again. It is a one-time step. | |
missing_permission | Facebook, Instagram | Facebook has not granted a permission this post needs. Reconnect the Page and accept every permission on the consent screen. |
missing_scope | TikTok | The TikTok connection is missing a permission it needs. Reconnect TikTok and accept every permission on the consent screen. |
needs_reauth | TikTok, Facebook, Instagram, X, LinkedIn, Bluesky, Threads | The TikTok connection has expired. Reconnect TikTok, then post again. |
no_channels | Any | The account has no connected channels at all, so there is nowhere to publish. Connect one first. GET /v1/connect-link returns a signed link a person can open to authorise a network; the API cannot complete an OAuth consent on its own. |
no_matching_accounts | Any | The requested platforms are real, but nothing is connected on them. Connect that network first, or pick one you already have from GET /v1/social-accounts. |
no_target | Any | The request named no destination: no accounts, and either no profile or a profile with nothing connected. Pass `accounts` with acc_… ids from GET /v1/social-accounts, or `profile` with a username that has channels connected. |
option_invalid | YouTube | That YouTube category id does not exist. Pick one YouTube lists for your region. |
pinterest_board_missing | That Pinterest board no longer exists or is not visible to this account. Pick another board. | |
pinterest_site_blocked | Pinterest could not fetch a media URL from our host. Republish: images and covers are uploaded as bytes now, not fetched by Pinterest. | |
post_state_invalid | Any | The post is not in a state that allows this operation. Check `state` on GET /v1/posts/{id}. Drafts are edited and deleted; scheduled posts are moved and cancelled; published posts are retracted. |
profile_required | Any | The account has more than one profile, so which to post to is ambiguous. Pass `profile` with one of the usernames from GET /v1/profiles. |
profile_unknown | Any | The named profile does not exist on this account. List them with GET /v1/profiles and use a `username` from there. |
quota_exceeded | YouTube | The daily YouTube API quota is spent. It resets at midnight Pacific time. |
rate_limited | TikTok, Bluesky | TikTok allows 6 requests a minute per account. Wait a minute and try again. |
schedule_in_past | Any | scheduledAt is not in the future. Send a future ISO timestamp, or omit `scheduledAt` to publish now. |
temporarily_blocked | Facebook has temporarily blocked this action, usually after too much activity too quickly. Wait a few hours and try again. | |
text_invalid | YouTube | YouTube rejected the description. Remove any < or > characters and keep it under 5,000. |
text_required | Any | The post has no text. Send a non-empty `text`. If the post is media-only, send a space or a caption: every network we publish to stores a caption field. |
text_too_long | X | The post is over X's character limit. Shorten it and try again. |
tiktok_account_restricted | TikTok | TikTok has restricted this account from creating posts. Only TikTok can lift that, so check the account's status in the TikTok app. |
tiktok_privacy_level | TikTok | Pick a privacy setting this account actually offers. Call the publish-info endpoint to see which ones are available to it. |
tiktok_spam_risk | TikTok | TikTok's anti-spam checks blocked this post. Slow the posting rate down and try again later. |
tiktok_too_many_pending | TikTok | TikTok caps you at 5 uploads awaiting action in any 24 hours. Finish or delete the pending ones in the TikTok app, then post again. |
tiktok_unaudited | TikTok | This TikTok app is still in limited mode: the account must be set to Private and the post visibility set to Only me. Change both in the TikTok app and try again. |
tiktok_url_unverified | TikTok | This is ours to fix, not yours. Our media domain needs re-verifying with TikTok. Contact support and we will sort it. |
timezone_invalid | Any | The timezone is not an IANA zone name. Use an IANA zone name, e.g. Europe/London or America/New_York. Abbreviations like GMT or PST are not accepted because several of them are ambiguous. |
timezone_without_schedule | Any | A timezone was sent with no scheduledAt to interpret. Drop `timezone`, or add `scheduledAt`. A timezone only means something when there is a time to interpret. |
title_invalid | YouTube | YouTube rejected the title. It cannot be empty, and cannot contain < or >. |
unknown_webhook_event | Any | At least one requested webhook event is unsupported. Use one of the documented webhook event names. |
webhook_https_required | Any | The webhook endpoint does not use HTTPS. Use an HTTPS endpoint. |
x_account_locked | X | X has locked this account. Sign in at x.com and complete their checks, then try again. |
x_unavailable | X | X posting is briefly unavailable on our side. Nothing is wrong with your post or your account. Try again shortly, and contact support if it persists. |
youtube_no_channel | YouTube | This Google account has no YouTube channel. Create one at youtube.com, then reconnect. |
youtube_upload_limit | YouTube | This channel has hit YouTube's upload limit for now. Wait a few hours and try again. |
Checking before you publish costs nothing: POST /v1/posts/validate runs the same checks and returns the same codes and fixes per target, without creating anything.
Error types
| Type | HTTP | Meaning | Retry? |
|---|---|---|---|
invalid_request | 400 | Malformed body or invalid field/option. | Fix & resend |
unauthorized | 401 | Missing or invalid API key. | No |
entitlement_exceeded | 402 | Out of credits, or your plan doesn't allow this (e.g. X/TikTok on free). | After topping up / upgrading |
rate_limited | 429 | Too many requests. | Yes, after a short back-off |
| platform errors | — | A specific network rejected the target (see platform). | Depends on the message |
Error codes
Alongside type, a validation error carries a granular code, the request field at fault in param, a plain-language fix, and a docs link back to this table. Branch on code: it is stable, and it is the only field specific enough to act on. type stays coarse on purpose.
{
"type": "invalid_input",
"message": "TikTok requires at least one media item",
"code": "media_required",
"fix": "TikTok cannot publish text on its own. Attach an image or a video and post again.",
"docs": "https://docs.postlake.dev/errors#media_required",
"param": "media",
"platform": "tiktok",
"retryable": false
}Codes we return before publishing
These are caught before anything is charged or published, so a post that fails them never costs a credit. Call POST /v1/posts/validate to get them without creating a post.
| Code | param | What it means | How to fix it |
|---|---|---|---|
media_required | media | The network cannot publish text on its own. | Attach at least one image or video. TikTok, Instagram, YouTube and Pinterest all require media. |
video_required | media | The network publishes video only. | Attach a video file. YouTube will not take a text or image post. |
video_not_supported | media | The network will not take a video on this post type. | Remove the video, or send an image or text-only version to that network. |
image_not_supported | media | The network will not take images on this post type. | Remove the images, or send the post to a network that supports them. |
media_type_unsupported | media | The file format is rejected outright by that network. | Convert the file. TikTok takes JPEG and WebP, and rejects PNG. |
image_too_large | media | The image is above the network's pixel ceiling. | Resize it. TikTok's ceiling is 1080x1920. Oversized carousels are accepted at upload and fail minutes later, which is why this is checked up front. |
image_too_small | media | The image is below the network's pixel floor. | Use a larger source image. Scaling up to clear the floor will look soft. |
text_too_long | text | The caption is over that network's character limit. | Shorten the text, or set a shorter caption for just that network with textOverrides. |
title_too_long | platformOptions.title | The title is over that network's limit. | Shorten the title. TikTok photo posts allow 90 characters. |
option_invalid | platformOptions.* | A platformOptions value is not one this network accepts. | Set it to one of the listed values. GET /v1/platforms returns the valid set for every option. |
needs_reauth | accounts | The connection to that network has expired. | Reconnect the channel in the dashboard, then post again. No amount of retrying will fix it. |
Codes the networks return
These come back after we have tried to publish. They carry a fix only where we can identify the error with confidence: an unrecognised rejection returns the network's own message and no advice, rather than a guess.
| Code | param | What it means | How to fix it |
|---|---|---|---|
missing_scope | accounts | The connection lacks a permission the post needs. | Reconnect and accept every permission on the network's consent screen. |
duplicate_post | text | The network refuses a post identical to a recent one. | Change the wording and post again. |
rate_limited | — | The network is throttling this account. | Back off and retry. TikTok allows 6 requests a minute per account. |
temporarily_blocked | — | Meta has temporarily blocked the action after too much activity. | Wait a few hours, then try again. |
quota_exceeded | — | The daily API quota for that network is spent. | Wait for the reset. YouTube's is midnight Pacific. |
tiktok_too_many_pending | — | TikTok caps you at 5 uploads awaiting action per 24 hours. | Finish or delete the pending uploads in the TikTok app. |
tiktok_account_restricted | — | TikTok has restricted the account from posting. | Only TikTok can lift this. Check the account in their app. |
tiktok_spam_risk | — | TikTok's anti-spam checks blocked the post. | Slow the posting rate and try again later. |
tiktok_unaudited | — | The TikTok app is in limited mode. | Set the account to Private and the post to Only me. |
link_unreachable | text | The network could not read a link in the post. | Check the URL opens publicly, then try again. |
media_too_large | media | The file is over the network's byte limit. | Compress it. Bluesky caps images near 1MB. |
media_unreadable | media | The network could not read the file. | Check it is a public, valid image or video. |
Retrying safely
When retryable is true (e.g. rate_limited), back off and retry. Always send the same Idempotency-Key on retries so a post that actually went out isn't duplicated. On 429, honour any Retry-After header. Non-retryable errors (unauthorized, invalid_request) need a fix, not a retry.
A 403 that isn't from us
If you get a 403 whose body is a Cloudflare page mentioning error 1010, that is our edge rejecting the request before it reaches the API. It is not an authentication problem, so checking your key will not help.
The usual cause is Python's standard library: urllib.request sends User-Agent: Python-urllib/3.x, which the bot rules block. Set any other agent and it goes through:
req.add_header("User-Agent", "my-app/1.0")
requests, httpx, curl, fetch and the Go client all send their own agent and are unaffected.