Native APIReference
PreviewErrors
How a Native API tool call fails: the error envelope, when to retry, and every error code with the fields that come with it.
Every way a Native tool can fail comes with a code. This page lists the codes a partner backend can meet, and what to do about each. Branch on the code. Never parse the message.
Native does not retry a failed publish on its own. A destination that failed stays failed until you act.
Where a failure shows up
#- The tool refused the call. The result has
isError: trueand carries the error envelope. - Part of the call failed. Some tools succeed as a whole and report failures per destination or per post. See failures inside a result.
- The request never reached a tool. A token problem or the edge rate limit answers with an HTTP status and no envelope. See errors outside the envelope.
- An argument broke the input schema. The MCP SDK rejects it before the tool runs, with its own validation error instead of the envelope. Treat that as a bug in your call.
The error envelope
#When a tool refuses a call, the result has isError: true, and structuredContent is { "data": <envelope> }. content[0].text holds the same envelope as a JSON string.
structuredContent.data
{
"ok": false,
"error": {
"code": "schedule_preflight_failed",
"message": "A sentence Native wrote for people",
"retryable": false,
"blockedDestinations": [
{
"platform": "INSTAGRAM",
"code": "caption_too_long",
"characterCount": 2410,
"characterLimit": 2200
}
]
}
}Envelope
okbooleanRequiredAlways
falseon a failure.errorobjectRequiredWhat went wrong.
codestringRequiredThe failure, as a stable code. Branch on this.
messagestringRequiredA sentence Native wrote for people. It can change at any time, so show it or log it, but never parse it.
retryablebooleanRequiredWhether the same call, unchanged, could succeed.
falsemeans something has to change first.
retryable answers one question: could the same call, sent again unchanged, succeed? It does not say the call did nothing. After a timeout, read the current state before you retry. See retrying.
Structured fields
#Some codes carry extra fields beside code, message and retryable. They sit on the same error object. Each one is named with the codes that use it. Destination names in these fields are uppercase Platform enum names, such as INSTAGRAM.
Fields on error
invalidFieldsstring[]OptionalThe input fields Native refused, as paths such as
reddit.subredditorplatforms.0. Onbad_request. New, in development: also onfont_not_foundandlogo_unreadable, which each refuse one argument.fieldErrorsobject[]OptionalOn
bad_request: a sentence for each refused field, where Native wrote one.invalidFieldsstill names every refused field, including any with no sentence here.fieldstringRequiredThe field path.
messagestringRequiredWhy it was refused.
blockedDestinationsobject[]OptionalOn
schedule_preflight_failed, and in areschedule_postsentry: every destination that will not publish the post as it stands.platformstringRequiredThe destination.
codestringRequiredThe rule it breaks. See pre-flight codes.
One of:
caption_too_long,media_required,image_aspect_invalid,too_many_images,video_required,video_not_supported,post_empty,reddit_subreddit_required,reddit_subreddit_not_found,reddit_flair_required,reddit_title_too_longcharacterCountintegerOptionalWith
caption_too_long: the caption’s length. Withreddit_title_too_long: the title’s length.characterLimitintegerOptionalWith
caption_too_longorreddit_title_too_long: the destination’s limit.imageCountintegerOptionalWith
too_many_images: how many pictures the post has.imageLimitintegerOptionalWith
too_many_images: how many the destination takes.
refusedDestinationsobject[]OptionalOn
schedule_failed: every destination the provider refused.platformstringRequiredThe destination.
codestringRequiredWhy it was refused. See codes per destination.
scheduledDestinationsstring[]OptionalOn
schedule_failed: the destinations that hold a booking even though another one refused.lockedDestinationsobject[]OptionalOn
already_scheduledandpost_already_scheduled: every destination a publishing provider is holding. Thealready_scheduledthatreject_postreturns carries none, so read eachplatformPostIdfromget_post_status.platformstringRequiredThe destination.
platformPostIdstringRequiredThe id
unschedule_platformtakes to withdraw it. One call per entry.
publishedPlatformsstring[]OptionalOn
post_already_publishedandalready_published: the destinations that are live.unavailablePlatformsstring[]OptionalOn
platform_not_connected: the destinations the brand has no live connection to.connectedPlatformsstring[]OptionalOn
platform_not_connected: everything the brand can publish to now. It may be empty.unsupportedPlatformsstring[]OptionalOn
platform_not_supported: the destinations Native does not publish to at all.supportedPlatformsstring[]OptionalOn
platform_not_supported: the destinations this brand could publish to.supportedChannelsstring[]OptionalNew, in development: on
platform_not_supportedfromcreate_connect_link, the channels Native can open a headless link for, in lowercase.overLimitPlatformsstring[]OptionalOn
caption_over_limit: the destinations whose limit the caption exceeds.characterCountintegerOptionalOn
caption_over_limit: the length of the caption you sent.characterLimitintegerOptionalOn
caption_over_limit: the limit the caption has to fit, the shortest among the destinations that refused it.resetDateISO 8601 stringOptionalOn
usage_limit_exceeded: when the workspace’s AI budget refills.jobIdstringOptionalOn
image_generation_in_progress: the render already running, forget_generation_job. Present when Native can say which render it is.websiteRefusalstringOptionalNew, in development: on
invalid_website_url, why the website was refused. See website refusals.One of:
BLOCKED_DOMAIN,URL_NOT_REACHABLE,URL_UNSAFE,URL_NOT_ALLOWED
Error codes
#These are the codes a whole call can fail with, grouped by what they are about. The retryable column is the value the envelope carries.
Codes any tool can return
#| Code | retryable | What it means |
|---|---|---|
bad_request | false | Invalid input. invalidFields names the refused fields, and fieldErrors says what is wrong with each where Native wrote a sentence. Fix those and call again. |
not_found | false | The id is wrong, or it is not yours. |
forbidden | false | This principal is not allowed to do this. |
subscription_required | false | The workspace has no active plan. |
writes_disabled | false | Writes are switched off on this deployment. Only tools that change data return it. |
internal_error | true | Native failed on its side. Retry shortly. |
upstream_timeout | true | Native did not answer within 120 seconds. The work may still be running, so read the current state before you retry. |
upstream_unavailable | true | Native could not be reached. Retry with backoff. |
upstream_identity_failed | true | The gateway could not authenticate itself to Native. Retry with backoff. |
identity_provider_unavailable | true | The gateway could not reach the identity provider to check your token, so the call was never authorised. You do not need a new token. Retry with backoff. |
upstream_429 | true | Native is rate limiting. Back off, then retry. |
upstream_<status> for another 4xx | false | Native refused the call with a code the gateway does not recognise, for example upstream_409. Change the call before you send it again. |
upstream_<status> for a 5xx | true | Native failed with a code the gateway does not recognise. Retry with backoff. |
Partner access
#| Code | retryable | What it means |
|---|---|---|
partner_only | false | New, in development: a partner-only tool was called without a partner principal. Nothing changed. |
partner_not_found | false | New, in development: the token is valid, but Native has no partner configured for its client id. Contact Native. Retrying will not help. |
partner_downstream_auth_failed | true | New, in development: the gateway could not get access to Native for your partner client. Your token is not the problem, so keep it. Retry with backoff. |
Profiles
#| Code | retryable | What it means |
|---|---|---|
profile_not_ready | true | New, in development: a generating tool was called on a profile whose create_profile build is building or failed. That covers create_posts, generate_caption, generate_image, regenerate_post_image, ad generation, and create_draft when it is sent a prompt or includeImage: true. Poll get_profile until onboarding.status is ready. If it is failed, retry the build first: call create_profile again with the same externalId. |
invalid_website_url | false | New, in development: create_profile refused the website before it created anything. websiteRefusal says why. |
stripe_billed_workspace | false | New, in development: deactivate_profile or delete_profile was called on a workspace that pays for its own Native subscription. That subscription has to be cancelled in Native. |
Branding
#| Code | retryable | What it means |
|---|---|---|
font_not_found | false | New, in development: update_brand_style named a font that is neither in the brand’s library nor a Google Fonts family. No role changed. invalidFields names fonts.heading or fonts.body. |
logo_unreadable | false | New, in development: the file given to upload_brand_logo is not a usable logo. It did not read as an image at least 16 px on each side, could not be fetched, or failed the upload scan. Nothing was saved. invalidFields names the argument. |
source_not_https | false | New, in development: upload_brand_logo was given a sourceUrl that is not https. Nothing was fetched. Send the same address as https. |
source_not_public | false | New, in development: upload_brand_logo was given a sourceUrl that points at a private or internal address. Nothing was fetched. Use an address anyone on the internet can reach. |
design_style_unavailable | false | New, in development: the brand is not on Design style, or its Design style cannot be read, so colour roles and fonts were not set. Palette colours can still be added on their own. |
design_style_busy | true | New, in development: another Design style save landed at the same moment, or Native was still setting up. Palette colours and library fonts the call added stay. Call again with the same arguments in a few seconds. |
Connecting accounts
#| Code | retryable | What it means |
|---|---|---|
platform_not_connected | false | The brand has no live connection to a destination the call named or, for scheduling and publishing, no connected channel at all. unavailablePlatforms and connectedPlatforms say which. Drop those destinations, or have the client connect the account. |
platform_not_supported | false | Native does not publish to a destination the call named, however the brand is connected. unsupportedPlatforms and supportedPlatforms list both sides. New, in development: from create_connect_link, supportedChannels lists the channels it can open instead. |
connect_session_expired | false | New, in development: the connectToken expired or its link died. Mint a fresh link with create_connect_link. |
headless_connect_unavailable | false | New, in development: Native could not mint a headless link for this brand. Call create_connect_link without platform and returnUrl for a link to Native’s connect page. |
Posts and generation
#| Code | retryable | What it means |
|---|---|---|
usage_limit_exceeded | false | The workspace’s AI budget is used up. Nothing AI-backed succeeds until resetDate. |
ai_generation_failed | true | AI generation failed. Retry. |
media_required | false | Every destination on the post is a story, and the post has no media. Attach a picture or video, or add a feed destination. |
caption_over_limit | false | The caption is longer than every destination on the post allows, so nothing was written. characterCount, characterLimit and overLimitPlatforms say by how much and where. |
reddit_options_required | false | A REDDIT destination needs reddit: { subreddit }, and none was sent. Send it, or drop REDDIT. |
image_generation_in_progress | false | A render is already running for this post. Poll it with get_generation_job, using jobId when the error carries one. Do not start another. |
template_rendering | false | New, in development: the workspace renders posts from design templates. Use generate_image instead of regenerate_post_image, and do not pass saveToLibrary. These pages do not cover generate_image yet: tools/list has its schema. |
post_already_published, already_published | false | Some or all of the post is live, so it cannot be changed. publishedPlatforms lists what is live. Take those down with unpublish_post, or make a new post. |
One refusal has no code of its own in the gateway today. When update_post tries to remove a destination that the provider is holding, the refusal arrives as upstream_409. Withdraw that destination with unschedule_platform first.
New, in development: this refusal arrives as platform_scheduled, with retryable: false. The fix is the same: withdraw the destination with unschedule_platform, then remove it.
Approval
#The approval gate runs before schedule_post and publish_post_now, and refuses the whole call. reschedule_posts runs it for each entry and reports a refusal in that entry’s moves[].error. post_content_changed comes from approve_post and reject_post. Approval explains the content digest behind these codes.
| Code | retryable | What it means |
|---|---|---|
approval_required | false | New, in development: the workspace requires approval and there is none on record. Partner workspaces require it by default. Record one with approve_post. |
approval_stale | false | The content changed after it was approved. Read the post again, have it reviewed again, and approve it again. |
approval_rejected | false | The latest decision is a rejection. Editing the post does not clear it. Only a new approve_post does. |
design_changed_after_approval | false | New, in development: on a workspace that requires approval, the post’s design changed after it was approved, or publishing would change the approved pictures with a design re-render or a media repair. Nothing was sent. Read it again with view_post, have it reviewed again, and approve it again. |
post_content_changed | false | The post changed between view_post and the decision. Read it again and review it again. Never resend with a newer contentDigest the reviewer did not see. |
Scheduling and publishing
#| Code | retryable | What it means |
|---|---|---|
schedule_preflight_failed | false | Native checked the post before it contacted any platform. Nothing was booked and the post is untouched. blockedDestinations names each problem. See pre-flight codes. |
schedule_failed | Varies | At least one destination was refused at the provider, and others may have booked. refusedDestinations and scheduledDestinations say which. See when a schedule partly fails. |
already_scheduled, post_already_scheduled | false | A publishing provider holds the post, so its schedule or its media cannot change this way. lockedDestinations lists each held destination. Withdraw each one with unschedule_platform, one call per entry. From reject_post, this code comes without lockedDestinations: read each destination’s platformPostId from get_post_status. |
ayrshare_error | true | The publishing provider refused or failed the request. Read get_post_status for the reason before you retry. |
When a schedule partly fails
#Scheduling runs per destination underneath, so schedule_failed can leave some destinations booked. scheduledDestinations lists them, and refusedDestinations lists the ones that refused, each with a code per destination.
The envelope’s retryable is false when any refusing destination has media_invalid, channel_not_connected, channel_needs_reauth or content_rejected. It is true otherwise. A retry books only the destinations still outstanding.
One exception: when the error carries neither refusedDestinations nor scheduledDestinations, Native could not say which destination refused, and retryable is false. Read get_post_status for the reason before you change anything.
structuredContent.data
{
"ok": false,
"error": {
"code": "schedule_failed",
"message": "A sentence Native wrote for people",
"retryable": true,
"refusedDestinations": [{ "platform": "INSTAGRAM", "code": "provider_unavailable" }],
"scheduledDestinations": ["FACEBOOK"]
}
}Here Facebook is booked and Instagram was refused because the provider was unavailable. Call schedule_post again unchanged, and it books only Instagram.
Failures inside a result
#Some tools succeed as a whole and report what failed inside the result. Each failure there carries a code and a message, and all but the dry-run entries carry retryable too.
publish_post_now:publishResults[].reason, one per destination. Onlypublishedmeans a destination is live.reschedule_posts:moves[].error, one per entry, with the same codes asschedule_post. The other entries still ran.get_post_batch_status:items[].error, one per post. Item codes areusage_limit_exceeded,ai_generation_failed,platform_not_connected,platform_not_supported,reddit_options_required,media_required,bad_requestandinternal_error.get_post_status:platformResults[].error, one per destination.get_generation_job:error, when a render failed.- New, in development:
schedule_postandpublish_post_nowwithdryRun: truereturnblocked[], what would stop each destination, withplatform,codeandmessage. Its codes include the pre-flight codes,channel_not_connectedandchannel_needs_reauth, and forpublish_post_nowthe skip reasons too. Other codes can appear, such asbrand_inactive.
Codes per destination
#These codes describe why one destination failed. They appear in platformResults[].error.code, publishResults[].reason.code and refusedDestinations[].code.
| Code | retryable | What to do |
|---|---|---|
channel_not_connected | false | Send the client a link from create_connect_link, then retry. |
channel_needs_reauth | false | The client has to reconnect the account. Send a link from create_connect_link, then retry. |
permission_denied | false | The account lacks a permission, for example a Page role. Reconnect with the right account. |
rate_limited | true | The platform is rate limiting. Retry later with backoff. |
provider_timeout | true | Check get_post_status first, because it may have gone through. Then retry. |
provider_unavailable | true | Retry later. |
media_required | false | Attach or generate media. |
media_invalid | false | Replace the image or video. The platform refused its aspect ratio, size, format or duration. |
content_rejected | false | The platform refused the content, for example on policy or length. Edit it, have it approved again, then retry. |
duplicate_content | false | The platform sees the post as a duplicate. Change the content, or drop the destination. |
validation_failed | false | Fix what the message names. Do not retry unchanged. |
unknown_error | false | Do not retry blindly: it may have published. Check get_post_status, and contact Native support if it is still unclear. |
Pre-flight codes
#Before schedule_post contacts any platform, Native checks each destination against that platform’s rules. A destination that fails a rule appears in blockedDestinations[] with one of these codes.
| Code | What it means |
|---|---|
caption_too_long | The destination’s caption is over its limit. characterCount and characterLimit say by how much. |
media_required | The destination publishes nothing without a picture or a video. |
image_aspect_invalid | A picture is outside Instagram’s feed band of 0.75 to 1.91. |
too_many_images | The post has more pictures than the destination takes. imageCount and imageLimit say by how many. |
video_required | The destination takes exactly one video and no pictures, as YouTube does. |
video_not_supported | The destination publishes no video at all. |
post_empty | The post has neither a caption nor a picture. |
reddit_subreddit_required | A Reddit destination has no subreddit. |
reddit_subreddit_not_found | Reddit does not know the subreddit. |
reddit_flair_required | The subreddit requires a flair and the post has none. The flair is chosen in the Native app. |
reddit_title_too_long | The Reddit title is over Reddit’s limit. characterCount and characterLimit can say by how much. |
Fix every destination named, or leave those destinations out with platforms, before you call again. Unchanged, the call fails the same way.
Publish results
#publish_post_now answers with publishResults[], one per destination. A destination that did not publish carries a reason:
| Code | Status | What it means |
|---|---|---|
publishing_in_progress | pending | The platform accepted it and is finishing in the background. Instagram does this routinely. Do not retry. Poll get_post_status. |
already_published | skipped | Already live. Publishing again would duplicate it. |
held_by_provider | skipped | Booked for a scheduled time. Withdraw it with unschedule_platform first if it should go out now. |
stuck_at_provider | skipped | The provider took it and never reported back. Retrying could duplicate it. Contact Native support. |
story_partially_published | skipped | Some story frames are live. A retry would duplicate them. |
not_requested | skipped | Not named in platforms. |
| A code per destination | failed | The destination failed. Follow its retryable. |
Image render codes
#When an image render fails, get_generation_job reports status: "failed" and an error with one of these codes. To retry a failed render, call regenerate_post_image again. Never do that while the job is still running.
| Code | retryable | What it means |
|---|---|---|
content_rejected | false | The image provider refused the request, for example on content policy. Change the prompt. |
quota_exhausted | false | The image provider refused the render on quota or billing. |
provider_timeout | true | The image provider timed out. |
provider_unavailable | true | The image provider was unavailable or rate limiting. |
generation_failed | true | The render failed for another reason. |
Build and connect failures
#Two flows report failure outside a tool result: a profile build that runs in the background, and an account connection that ends in the client’s browser. Both are new and in development.
Profile build failures
#New, in development: when a create_profile build fails, get_profile reports onboarding.status: "failed" and a code in onboarding.error: website_unreachable, analysis_failed, enrichment_failed, build_not_queued or build_interrupted. Treat the list as open: more codes can appear. create_profile answers status: "failed" only when it could not start the build, in practice with build_not_queued.
To retry a failed build, call create_profile again with the same externalId. The build restarts where it stopped and the call answers building, so read the code from get_profile before you retry.
Website refusals
#New, in development: when create_profile refuses the website with invalid_website_url, nothing is created and websiteRefusal says why:
websiteRefusal | What it means |
|---|---|
BLOCKED_DOMAIN | A social media or platform address, not the client’s own website. |
URL_NOT_REACHABLE | The address does not resolve, or refused the connection. |
URL_UNSAFE | The address is flagged as harmful. |
URL_NOT_ALLOWED | The address points at a private address. |
Connect failures
#New, in development: a headless connect link sends the browser back to your returnUrl with status=failed and an error code, for example oauth_denied, link_expired, account_in_another_workspace or connection_failed. Mint a fresh link to retry. Do not poll: a failed connect does not recover on its own.
Errors outside the envelope
#These answers come back as an HTTP status before any tool runs, so they carry no envelope.
| Status | When | What to do |
|---|---|---|
401 | The token is missing, expired or invalid. The response carries a WWW-Authenticate challenge. | Fetch a new token. See Authentication. |
403 insufficient_scope | A tool that changes data was called with a token that lacks mcp:write. | Request the token with mcp:read mcp:write. |
429 | Too many requests from one IP address in a minute. The edge refuses the request before it reaches a tool. | Slow down and back off. See Limits. |
An argument that breaks a tool’s input schema is rejected by the MCP SDK before the tool runs. You get the SDK’s validation error, not the envelope. Check your call against the inputSchema that tools/list returns.
Retrying
#- Respect
retryable. Never send a call again afterretryable: falsewithout changing something first. - After a timeout or a dropped connection on a write, read the current state before you retry, with
get_post_status,get_post_batch_statusorview_post. A lost response does not prove that nothing changed. publish_post_nowis safe to call again. It retries only the destinations that failed and never dispatches a published one again. The skip reasonsstuck_at_providerandstory_partially_publishedneed Native support or a new post, not a retry.- A partly refused
reschedule_postscall is not untouched: the other entries ran. Do not resend the whole list. - Never call
regenerate_post_imageagain to retry a render that is still running. Poll its job withget_generation_job. - Back off exponentially, with jitter, on errors that are retryable. Stop on errors that are not.
Repeated calls are not deduplicated
#For each tool call, the gateway attaches an Idempotency-Key to every write it makes to Native, and Native replays the stored result for a repeated key. The transport keeps no state between calls, though, so each tool call gets a fresh random part in its key. If you send the same tool call again, it is a new attempt, and Native does not deduplicate it.
Caller-supplied idempotency keys are planned, not available. Until then, check the current state before you retry any write that creates or publishes something: create_posts, publish_post_now and schedule_post.
New, in development: create_profile is idempotent on its own terms. A repeat with the same externalId answers with the profile the first call made, with created: false.
Codes you do not recognise
#New codes can appear at any time for new failure cases. Treat a code you do not recognise as its retryable flag says.
Example
#A call with the official MCP SDK client, branching on the code:
TypeScript
const result = await client.callTool(
{
name: "schedule_post",
arguments: { id: postId, scheduledAt: "2026-11-02T09:00:00+01:00" },
},
undefined,
{ timeout: 150_000 },
);
const data = (result.structuredContent as { data: any } | undefined)?.data;
if (result.isError) {
const error = data.error as {
code: string;
message: string;
retryable: boolean;
blockedDestinations?: { platform: string; code: string }[];
};
switch (error.code) {
case "schedule_preflight_failed":
// Nothing was booked. Fix every destination it names, then call again.
for (const blocked of error.blockedDestinations ?? []) {
console.warn(blocked.platform, blocked.code);
}
break;
case "approval_stale":
// Read the post again with view_post and have it reviewed again.
break;
default:
if (error.retryable) {
// Read the current state first, then retry with backoff.
} else {
// Change something before calling again. Never parse error.message.
}
}
}The Native API is not open yet
Partner access is being built now. Tell us what you would build, and we will be in touch when it opens.