# Scheduling and publishing Source: https://docs.native.no/native-api/publishing > **Preview.** The Native API is not live yet. This page describes the planned interface; names and fields can change before access opens. Book a post for a time, move many at once, publish now, withdraw one destination, and follow each destination to a live link or a failure. > **Partner access is not live yet.** The tools on this page are in Native’s gateway today, for people signed in to Native. Partner access with client-credentials tokens is in development, so a partner backend cannot call them yet. Anything marked **in development**, such as `dryRun`, is not deployed yet and can still change. [Authentication](https://docs.native.no/authentication) explains how partner access will work. A post has one or more destinations, such as `INSTAGRAM` and `FACEBOOK`. Scheduling and publishing happen per destination: one can be live while another failed. So read the per-destination results, never one flag for the whole post. Native does not retry a failed publish on its own. A failed destination stays failed until you act. ## Before you schedule or publish - The brand needs a connected channel. `get_profile` reports it as `readiness.canPublish`. - The post has to pass the approval check. See [review and approval](https://docs.native.no/native-api/approval). - New, in development: you can rehearse the call first with [`dryRun: true`](https://docs.native.no/native-api/publishing#dry-run). Calls that contact platforms can take far longer than others. The gateway waits up to 120 seconds for Native before it answers `upstream_timeout`, so set your client’s timeout above that, for example 150 seconds. See [limits](https://docs.native.no/native-api/limits). ## The approval check `schedule_post` and `publish_post_now` check the post’s approval first. They refuse the whole call with `approval_rejected` or `approval_stale`. New, in development: also with `approval_required` or `design_changed_after_approval`. [Review and approval](https://docs.native.no/native-api/approval#approval-gate) explains each case. `reschedule_posts` checks each post on its own. A refusal shows up in that entry’s `moves[].error`, and the other entries still run. So a partly refused call is not untouched: do not resend the whole list. ## Schedule a post MCP tool `schedule_post` (preview) Books a post, or some of its destinations, for a time. It also moves a post that is already scheduled. That is one call, and the post stays booked throughout. Input: - `id` (string, required): The post id. - `scheduledAt` (ISO 8601 string, required): When to publish, with `Z` or a numeric offset, such as `2026-11-02T09:00:00+01:00`. Native stores it in UTC. A time in the past is refused on a post that is already scheduled. - `platforms` (string[], optional): Book only these destinations, as uppercase names such as `INSTAGRAM`. At least one when sent. Leave it out to book every destination. Destinations you do not name keep their own schedule. - `dryRun` (boolean, optional): New, in development: `true` runs every check and books nothing. See [rehearse with a dry run](https://docs.native.no/native-api/publishing#dry-run). Native stores the time before it hands the post to the platform. If no platform takes it, the post can carry a future date and still never publish. `scheduleConfirmed` is how you tell the two apart. Output: - `postId` (string, required): The post id. - `scheduleConfirmed` (boolean, required): Report the post as scheduled only when this is `true`. `false` means nothing confirms a platform holds the time, or a move did not fully land. - `scheduleWarning` (string, optional): Why the schedule could not be confirmed. Present when `scheduleConfirmed` is `false`. - `platformResults` (object[], optional): One entry per destination, as in [`get_post_status`](https://docs.native.no/native-api/publishing#platform-results). May be absent if reading the post back failed. - `publishedUrls` (string[], optional): Live links. Usually empty after scheduling. May be absent if reading the post back failed. No output schema is declared for the real call. The result also carries the post as it now reads, and the `view_post` preview fields (`authorName`, `authorSubtitle`, `authorAvatarUrl`, `postText`, `imageUrl`, `imageUrls`, `destinations`, `appOrigin`, `appUrl`), any of which may be absent. Ignore fields you do not use. Schedule a post: ```ts 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 }).data; if (result.isError) { // data = { ok: false, error: { code, message, retryable, ... } } handleScheduleError(data.error); } else if (data.scheduleConfirmed) { // Booked. Poll get_post_status a few minutes after the time. } else { // Not confirmed. Show data.scheduleWarning, then check get_post_status. } ``` | Code | Retryable | Meaning | Extra fields | | --- | --- | --- | --- | | `schedule_preflight_failed` | `false` | Checked before any platform was contacted. Nothing was booked and the post is untouched. Fix every destination named, or leave them out with `platforms`, then call again. | `blockedDestinations[]` | | `schedule_failed` | Varies | At least one destination refused at the provider. Others may have booked. See [when a destination refuses](https://docs.native.no/native-api/publishing#schedule-failed). | `refusedDestinations[]`, `scheduledDestinations[]` | | `platform_not_connected` | `false` | The post has no destinations, and the brand has no connected channel. Send the client a link from `create_connect_link`. A destination whose own account is not connected shows up as `schedule_failed` with `channel_not_connected` instead. | `unavailablePlatforms`, `connectedPlatforms` | | `approval_rejected` | `false` | The latest decision is a rejection. | None | | `approval_stale` | `false` | The content changed after it was approved. | None | | `approval_required` | `false` | New, in development: the workspace requires approval and none is on record. | None | | `design_changed_after_approval` | `false` | New, in development: publishing would change the approved pictures. Nothing was sent. | None | | `post_already_published`, `already_published` | `false` | A destination of the post is live, so the post cannot be scheduled. Which of the two codes you get depends on the post’s state, so handle both. | `publishedPlatforms` | Common codes, such as `bad_request` and `upstream_timeout`, are listed under [errors](https://docs.native.no/native-api/errors). ### Checks before any platform is contacted Native checks what each destination will take before it contacts any platform. If any destination fails, the call fails with `schedule_preflight_failed` and lists every blocked destination in one answer. The same call, unchanged, fails the same way: it is a list of edits to make, not a thing to retry. structuredContent.data: ```json { "ok": false, "error": { "code": "schedule_preflight_failed", "message": "Gateway-written sentence for people", "retryable": false, "blockedDestinations": [ { "platform": "INSTAGRAM", "code": "caption_too_long", "characterCount": 2410, "characterLimit": 2200 } ] } } ``` blockedDestinations[] entry: - `platform` (string, required): The blocked destination, such as `INSTAGRAM`. - `code` (string, required; 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_long`): The rule it breaks. Treat an unknown code as blocking. - `characterCount` (integer, optional): With `caption_too_long`: the caption’s length. - `characterLimit` (integer, optional): With `caption_too_long`: this destination’s limit. - `imageCount` (integer, optional): With `too_many_images`: how many images the post has. - `imageLimit` (integer, optional): With `too_many_images`: how many this destination takes. | Code | What it means | What to do | | --- | --- | --- | | `caption_too_long` | The caption is over this destination’s limit. | Shorten this destination’s own copy with `update_post` and `platform`, or rewrite it with `generate_caption`. | | `media_required` | The destination publishes nothing without a picture, as stories do. | Attach or generate an image. | | `image_aspect_invalid` | A picture is outside Instagram’s feed band of 0.75 to 1.91. | Replace it with a picture inside the band. | | `too_many_images` | The post has more images than this destination takes. | Remove images down to `imageLimit`. | | `video_required` | The destination takes exactly one video and no pictures, as YouTube does. | Use a post with one video, or leave the destination out. | | `video_not_supported` | The destination publishes no video. | Leave the destination out, or use pictures. | | `post_empty` | There is neither a caption nor media. | Add copy or a picture. | | `reddit_subreddit_required` | A Reddit destination needs a subreddit. | Set one with `update_post` and `reddit`. | | `reddit_subreddit_not_found` | The subreddit does not exist. | Name another subreddit. | | `reddit_flair_required` | The subreddit requires a flair, and the post has none. | The flair is chosen in the Native app. Send the client to the post’s `appUrl`. | | `reddit_title_too_long` | The Reddit title is over Reddit’s limit. | Shorten the title. | ### When a destination refuses `schedule_failed` means at least one destination refused at the provider. Others may have booked anyway. - `refusedDestinations[]` lists each refusal as `{ platform, code }`, with the codes under [failure codes](https://docs.native.no/native-api/publishing#failure-codes). - `scheduledDestinations[]` lists the destinations that booked. - The envelope’s `retryable` is `false` if any refusal is `media_invalid`, `channel_not_connected`, `channel_needs_reauth` or `content_rejected`, and `true` otherwise. When `retryable` is `true`, call `schedule_post` again unchanged. It books only the destinations still outstanding. When it is `false`, fix what the codes name first. If there is no `refusedDestinations`, read `get_post_status` for the reason before you change anything. ## Schedule several posts MCP tool `reschedule_posts` (preview) Schedules or moves up to 25 posts in one call. Despite the name, it books drafts for the first time too. Entries run in the order you send them, and one failure does not stop the rest. Each entry gets the same checks as `schedule_post`, including the pre-flight and the approval check. `reschedule_posts` has no `dryRun`. Input: - `moves` (object[], required): The posts to book or move. 1 to 25 entries. - `postId` (string, required): The post id. - `scheduledAt` (ISO 8601 string, required): When to publish this post, with `Z` or a numeric offset. - `platforms` (string[], optional): Book only these destinations of this post. At least one when sent. Leave it out to book them all. Output: - `moves` (object[], required): One entry per entry you sent, in the same order. - `postId` (string, required): The post id. - `scheduledAt` (string, required): The time as you sent it. - `status` (string, required; one of: `rescheduled`, `unconfirmed`, `failed`): `rescheduled`: a platform holds the time, for a first booking as much as a move. `unconfirmed`: Native accepted the time, but nothing confirms a platform holds it. `failed`: see `error`. - `platformResults` (object[], optional): This post’s destinations, as in [`get_post_status`](https://docs.native.no/native-api/publishing#platform-results). Also on a failed entry the provider refused. - `scheduleWarning` (string, optional): Why an `unconfirmed` entry is not confirmed. - `error` (object, optional): On a `failed` entry. The codes are the same as `schedule_post`’s. - `code` (string, required): Branch on this. - `message` (string, required): A sentence for people. It can change at any time. - `retryable` (boolean, required): Whether this entry, unchanged, could succeed. - `refusedDestinations` (object[], optional): With `schedule_failed`: each refusal as `{ platform, code }`. - `scheduledDestinations` (string[], optional): With `schedule_failed`: the destinations that booked anyway. - `blockedDestinations` (object[], optional): With `schedule_preflight_failed`: the [blocked destinations](https://docs.native.no/native-api/publishing#preflight). Nothing was booked for this post. - `rescheduled` (integer, required): How many entries have `status: "rescheduled"`. - `failed` (integer, required): How many entries have `status: "failed"`. An `unconfirmed` entry counts in neither total, so `rescheduled` plus `failed` can be less than the number of entries. structuredContent.data: ```json { "moves": [ { "postId": "3f6a2c1e-8b4d-4f7a-9e2b-5c0d1a7e4b93", "scheduledAt": "2026-11-02T09:00:00+01:00", "status": "rescheduled", "platformResults": [ { "platformPostId": "a1c9e7b2-4d6f-4a8e-b3c5-0f2d9e1a7b64", "platform": "FACEBOOK", "state": "scheduled", "scheduledAt": "2026-11-02T08:00:00.000Z", "url": null, "error": null } ] }, { "postId": "7b0e5d9a-2c4f-4e1b-8a6d-3f9c0b2e5a71", "scheduledAt": "2026-11-03T09:00:00+01:00", "status": "failed", "error": { "code": "approval_stale", "message": "Gateway-written sentence for people", "retryable": false } } ], "rescheduled": 1, "failed": 1 } ``` After a partly failed call, fix the failed entries and send only those. Check `unconfirmed` entries with `get_post_status` before you send them again. ## Publish now MCP tool `publish_post_now` (preview) Publishes now. Calling it again retries only the destinations that failed. A destination that already published is never dispatched again. The post goes live on the client’s real accounts. `unpublish_post` can take some destinations down again, but Native cannot remove a post from Instagram, TikTok or a story yet. Treat publishing as final. Input: - `id` (string, required): The post id. - `platforms` (string[], optional): Publish or retry only these destinations. At least one when sent. Naming a destination the post does not have is a `bad_request`. - `dryRun` (boolean, optional): New, in development: `true` runs every check and publishes nothing. See [rehearse with a dry run](https://docs.native.no/native-api/publishing#dry-run). Output: - `postId` (string, required): The post id. - `publishResults` (object[], required): One entry per destination. - `platform` (string, required): The destination, such as `INSTAGRAM`. - `status` (string, required; one of: `published`, `pending`, `failed`, `skipped`): Only `published` means the destination is live. - `url` (string | null, required): The live link, once published. - `reason` (object | null, required): Why the destination is not published. `null` when it is. - `code` (string, required): Branch on this. - `message` (string, required): A sentence for people. It can change at any time. - `retryable` (boolean, required): Whether the same call, unchanged, could succeed. - `published` (integer, required): How many destinations are live. - `failed` (integer, required): How many destinations failed. - `skipped` (integer, required): How many destinations were skipped. - `pending` (integer, optional): How many destinations are still finishing. May be absent. - `platformResults` (object[], optional): As in [`get_post_status`](https://docs.native.no/native-api/publishing#platform-results). May be absent. - `publishedUrls` (string[], optional): Live links. May be absent. The result can also carry the `view_post` preview fields. Any of them may be absent. structuredContent.data: ```json { "postId": "3f6a2c1e-8b4d-4f7a-9e2b-5c0d1a7e4b93", "publishResults": [ { "platform": "FACEBOOK", "status": "published", "url": "https://www.facebook.com/123456789012345/posts/987654321098765", "reason": null }, { "platform": "INSTAGRAM", "status": "pending", "url": null, "reason": { "code": "publishing_in_progress", "message": "Sentence for people", "retryable": false } } ], "published": 1, "failed": 0, "skipped": 0, "pending": 1 } ``` | Code | Status | What it means | What to do | | --- | --- | --- | --- | | `publishing_in_progress` | `pending` | The platform accepted the post and is finishing it on its own. Instagram does this routinely. | Do not retry. Poll `get_post_status`, where the destination reads `scheduled` until it lands. | | `already_published` | `skipped` | The destination is already live. | Nothing. Publishing again would duplicate it. | | `held_by_provider` | `skipped` | The destination is booked for a scheduled time. | If it should go now, call `unschedule_platform` on it first. | | `stuck_at_provider` | `skipped` | The provider took the destination and never reported back. | Do not retry: it could duplicate the post. Contact Native support. | | `story_partially_published` | `skipped` | Some frames of the story are live. | Do not retry: it would send every frame again. Make a new post from the frames that did not publish. | | `not_requested` | `skipped` | The destination was not named in `platforms`. | Nothing. | | A [failure code](https://docs.native.no/native-api/publishing#failure-codes) | `failed` | The destination did not publish. | Follow `retryable`. | | Code | Retryable | Meaning | | --- | --- | --- | | `approval_rejected` | `false` | The latest decision is a rejection. Nothing was published. | | `approval_stale` | `false` | The content changed after it was approved. Nothing was published. | | `approval_required` | `false` | New, in development: the workspace requires approval and none is on record. | | `design_changed_after_approval` | `false` | New, in development: publishing would change the approved pictures. Nothing was sent. | | `bad_request` | `false` | The input is invalid, for example a destination the post does not have, or a post with no destinations. | An account that is not connected does not fail the whole call. That destination comes back `failed` with `channel_not_connected` or `channel_needs_reauth` in `publishResults`, while the others go out. ## Rehearse with a dry run > **In development.** `dryRun` is in development and not deployed. Today the input schema does not allow it, so the MCP SDK rejects a call that sends it before the tool runs. `schedule_post` and `publish_post_now` will take `dryRun: true`. Native runs every check the real call runs and stops before its first write. Nothing is booked, published, written or sent to any platform. A dry run needs the same `mcp:write` scope as the real call. schedule_post output with dryRun: - `dryRun` (boolean, required): Always `true`. - `postId` (string, required): The post id. - `scheduledAt` (ISO 8601 string, required): The time you sent, in UTC. - `wouldSchedule` (string[], required): The destinations the real call would book. - `blocked` (object[], required): One entry per destination the real call would not book. - `platform` (string, required): The destination that would not go out, such as `INSTAGRAM`. - `code` (string, required): Why. Branch on this. - `message` (string, required): A sentence for people, written by Native. It can change at any time. - `characterCount` (integer, optional): With `caption_too_long`: the caption’s length. - `characterLimit` (integer, optional): With `caption_too_long`: this destination’s limit. - `imageCount` (integer, optional): With `too_many_images`: how many images the post has. - `imageLimit` (integer, optional): With `too_many_images`: how many this destination takes. publish_post_now output with dryRun: - `dryRun` (boolean, required): Always `true`. - `postId` (string, required): The post id. - `wouldPublish` (string[], required): The destinations the real call would dispatch. - `blocked` (object[], required): One entry per destination the real call would not publish, in the same shape as for `schedule_post`. - `platform` (string, required): The destination that would not go out, such as `INSTAGRAM`. - `code` (string, required): Why. Branch on this. - `message` (string, required): A sentence for people, written by Native. It can change at any time. - `characterCount` (integer, optional): With `caption_too_long`: the caption’s length. - `characterLimit` (integer, optional): With `caption_too_long`: this destination’s limit. - `imageCount` (integer, optional): With `too_many_images`: how many images the post has. - `imageLimit` (integer, optional): With `too_many_images`: how many this destination takes. schedule_post with dryRun: structuredContent.data: ```json { "dryRun": true, "postId": "3f6a2c1e-8b4d-4f7a-9e2b-5c0d1a7e4b93", "scheduledAt": "2026-11-02T08:00:00.000Z", "wouldSchedule": ["FACEBOOK"], "blocked": [ { "platform": "INSTAGRAM", "code": "caption_too_long", "message": "Gateway-written sentence for people", "characterCount": 2410, "characterLimit": 2200 } ] } ``` | Code | Call | What it means | | --- | --- | --- | | The [pre-flight codes](https://docs.native.no/native-api/publishing#preflight) | Both | What the destination will not take. The real `publish_post_now` does not run these checks, so it would only learn them as a failed destination while the rest went live. | | `channel_not_connected` | Both | No account is connected for this platform. | | `channel_needs_reauth` | Both | The account was disconnected and the client has to reconnect it. | | `brand_inactive` | Both | The brand is not active, so nothing publishes for it. | | `platform_not_supported` | Both | This destination cannot publish through the brand’s current provider. | | `tiktok_video_not_public` | Both | This TikTok account can only post videos as public. | | `tiktok_photo_ai_label` | Both | This TikTok account cannot post photos marked as AI-generated. | | `publishing_in_progress` | `schedule_post` | A booked destination being moved is publishing right now, so it cannot be moved. | | `already_published`, `held_by_provider`, `stuck_at_provider`, `story_partially_published` | `publish_post_now` | The skip reasons of the real call, described under [publish now](https://docs.native.no/native-api/publishing#publish-now). | Treat the codes as an open set. A code you do not know still means the destination would not go out. Refusals of the whole post stay errors, and a dry run fails with them exactly as the real call would. For both tools: the approval check, a post with no destinations, and an unknown platform in `platforms`. For `schedule_post` also: a post with a live destination, and moving a booked post into the past. In a publish dry run, destinations you did not name in `platforms` are left out. An empty `blocked` list is not a guarantee. A few checks need the platform itself, so they run only in the real call, and a platform can still refuse at dispatch. A dry run reads the post as it stands at that moment, so run it close to the real call. Rehearse, then schedule: ```ts async function call(name: string, args: Record) { const result = await client.callTool( { name, arguments: args }, undefined, { timeout: 150_000 }, ); const data = (result.structuredContent as { data: any } | undefined)?.data; return { isError: result.isError === true, data }; } const when = "2026-11-02T09:00:00+01:00"; const rehearsal = await call("schedule_post", { id: postId, scheduledAt: when, dryRun: true, }); if (rehearsal.isError) { handleScheduleError(rehearsal.data.error); // for example approval_stale } else if (rehearsal.data.blocked.length > 0) { // Show rehearsal.data.blocked to the client and fix those destinations. } else { const booked = await call("schedule_post", { id: postId, scheduledAt: when }); // Check booked.isError and booked.data.scheduleConfirmed as usual. } ``` ## Withdraw a destination MCP tool `unschedule_platform` (preview) Withdraws one destination from the provider. A post with three destinations takes three calls. The destination keeps its date, as a draft. To clear the date as well, call `move_platform_to_drafts` with the same `platformPostId`. Input: - `platformPostId` (string, required): The destination’s own id. Get it from `platformResults[].platformPostId` in `get_post_status`, or from a `lockedDestinations` entry in an error. Output: - `unscheduleConfirmed` (boolean, required): Treat the destination as withdrawn only when this is `true`. - `unscheduleWarning` (string, optional): Why the withdrawal could not be confirmed. Present when `unscheduleConfirmed` is `false`. - `stillScheduled` (string[], optional): The post’s other destinations that a provider still holds. - `keptScheduledAt` (string | null, optional): The date the withdrawn destination kept. - `postId` (string, optional): The post id. - `platformResults` (object[], optional): As in [`get_post_status`](https://docs.native.no/native-api/publishing#platform-results). - `publishedUrls` (string[], optional): Live links. No output schema is declared. The result also carries the post as it now reads. | Code | Retryable | Meaning | | --- | --- | --- | | `already_published` | `false` | A destination of the post is live, even one you did not name. Take it down with `unpublish_post` instead. | | `not_found` | `false` | The `platformPostId` is wrong, or not yours. | ## Follow each destination MCP tool `get_post_status` (preview) A status snapshot of a post, per destination. Use it to poll after scheduling or publishing. Input: - `id` (string, required): The post id. Output: - `id` (string, required): The post id. - `postId` (string, required): The same value as `id`. - `status` (string, required; one of: `Draft`, `Scheduled`, `Published`, `partially_published`, `error`): A summary for the whole post. Branch on `platformResults` instead. - `scheduledAt` (ISO 8601 string | null, required): The earliest time across the destinations. - `platformResults` (object[], required): One entry per destination. See [results per destination](https://docs.native.no/native-api/publishing#platform-results). - `publishedUrls` (string[], required): Live links. - `contentDigest` (string | null, optional): New, in development: the version an approval locks, the same value `view_post` returns. Until then, read it from `view_post`. - `approval` (object | null, optional): New, in development: the latest review decision, as in [the approval object](https://docs.native.no/native-api/approval#approval-object). Until then, read it from `view_post`. - `platformVariants` (object[], required): The raw form of each destination. Prefer `platformResults`. - `platform` (string, required): The destination. - `publishingStatus` (string | null, required): The raw publishing status. - `socialPostUrl` (string | null, required): The live link, when there is one. - `publishErrorMessage` (string | null, required): A sentence about a failure. Do not branch on it. ### Results per destination platformResults[] entry: - `platformPostId` (string, optional): The destination’s own id, which `unschedule_platform` and `move_platform_to_drafts` take. May be absent. - `platform` (string, required): The destination, as an uppercase name such as `INSTAGRAM`. - `state` (string, required; one of: `published`, `failed`, `stuck`, `scheduled`, `pending`, `unknown`): What this destination is doing. See the table below. - `scheduledAt` (string | null, optional): This destination’s own time. The post’s `scheduledAt` is the earliest across destinations, so the two can differ. May be absent. - `url` (string | null, required): The live link, once published. - `error` (object | null, required): Present on a failure. The codes are under [failure codes](https://docs.native.no/native-api/publishing#failure-codes). - `code` (string, required): Branch on this. - `message` (string, required): A sentence for people. It can change at any time. - `retryable` (boolean, required): Whether the same call, unchanged, could succeed. | State | What it means | What to do | | --- | --- | --- | | `published` | The destination is live. `url` is the link. | Nothing. | | `scheduled` | It is booked for its time. A destination the platform accepted from `publish_post_now` and is still finishing also reads `scheduled` until it lands. | Wait, and poll after its time. | | `pending` | No provider holds it yet: it is not booked, published or failed. A draft destination reads `pending`, and so does one you withdrew with `unschedule_platform`. | Schedule or publish it if it should go out. It will not go out on its own. | | `failed` | It did not publish. `error` says why. | Follow `error.retryable`. Native does not retry on its own. | | `stuck` | The provider accepted it, and its time passed with no outcome. | Stop polling and contact Native support. Do not publish again: it may have gone out. | | `unknown` | Native could not work out the state. The destination may be live. | Read the status again later. Do not publish again on this alone. | structuredContent.data: ```json { "id": "3f6a2c1e-8b4d-4f7a-9e2b-5c0d1a7e4b93", "postId": "3f6a2c1e-8b4d-4f7a-9e2b-5c0d1a7e4b93", "status": "partially_published", "scheduledAt": "2026-11-02T08:00:00.000Z", "platformResults": [ { "platformPostId": "a1c9e7b2-4d6f-4a8e-b3c5-0f2d9e1a7b64", "platform": "FACEBOOK", "state": "published", "scheduledAt": "2026-11-02T08:00:00.000Z", "url": "https://www.facebook.com/123456789012345/posts/987654321098765", "error": null }, { "platformPostId": "d4e8b1f6-9a2c-4c7e-8f0b-6a3d5e9c1b27", "platform": "INSTAGRAM", "state": "failed", "scheduledAt": "2026-11-02T08:00:00.000Z", "url": null, "error": { "code": "media_invalid", "message": "Sentence for people", "retryable": false } } ], "publishedUrls": ["https://www.facebook.com/123456789012345/posts/987654321098765"], "platformVariants": [ { "platform": "FACEBOOK", "publishingStatus": "SUCCESS", "socialPostUrl": "https://www.facebook.com/123456789012345/posts/987654321098765", "publishErrorMessage": null }, { "platform": "INSTAGRAM", "publishingStatus": "FAILED", "socialPostUrl": null, "publishErrorMessage": "Sentence for people" } ] } ``` ### Failure codes per destination These codes appear in `platformResults[].error.code`, `publishResults[].reason.code` and `refusedDestinations[].code`. Treat a code you do not know as its `retryable` flag says. | 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 same: the client has to reconnect the account. | | `permission_denied` | `false` | The account lacks a permission, such as a page role. Reconnect with the right account. | | `rate_limited` | `true` | Retry later, with backoff. | | `provider_timeout` | `true` | Retry, but read `get_post_status` first, because it may have gone through. | | `provider_unavailable` | `true` | Retry later. | | `media_required` | `false` | Attach or generate media. | | `media_invalid` | `false` | Replace the image or video. The aspect ratio, size, format or duration is wrong for the platform. | | `content_rejected` | `false` | The platform refused the content, for example for policy or length. Edit it, have it approved again, and retry. | | `duplicate_content` | `false` | The platform sees it 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. Read `get_post_status`, and contact support if it is unclear. | ## How often to poll | What | Call | How often | Done when | | --- | --- | --- | --- | | A scheduled publish | `get_post_status` | Once a few minutes after `scheduledAt`, then every 5 minutes for 30 minutes. | Every destination is `published` or `failed`. While one still reads `scheduled`, keep polling. On `stuck`, stop and contact support. | | After `publish_post_now` | `get_post_status` | Every 10 seconds for 2 minutes, then every 60 seconds for 10 minutes. | Every destination is `published` or `failed`. Only the destinations that came back `pending` in `publishResults` need polling. They read `scheduled` here until they land. | | Channel health | `get_profile` | Daily, and before you schedule. | An ongoing check. Act on `needs_reauth`, `disconnected` and `expiring_soon` before publishing breaks. | Webhooks are not available, so polling is the only way to learn the outcome. Back off with jitter on errors that are `retryable`, and stop on ones that are not. ## Retry safely - Respect `retryable`. Never repeat a call whose `retryable` is `false` without changing something first. - `publish_post_now` is safe to call again. It retries only failed destinations and never dispatches a published one again. `stuck_at_provider` and `story_partially_published` need support or a new post, not a retry. - After a timeout or a dropped connection on a write, read `get_post_status` before you retry. A lost response does not prove that nothing changed. - Sending the same tool call again is a new attempt, and Native does not deduplicate it. Keys you supply yourself are planned, not available. Until then, check the state before you retry `schedule_post` or `publish_post_now`. - `upstream_timeout` means Native did not answer within 120 seconds. The work may still be running, so read the state first.