# Editing posts Source: https://docs.native.no/native-api/editing-posts > **Preview.** The Native API is not live yet. This page describes the planned interface; names and fields can change before access opens. Change a post’s caption, first comment, destinations or a single picture, and follow a picture render until it lands. > **Not open to partners yet.** Partners cannot call the Native API yet. Partner sign-in with client credentials is still in development. Tools marked **Preview** run in Native’s gateway today, for people signed in to Native. Anything marked **In development** is not deployed yet. You can change a draft or a scheduled post until it is published. Edits overwrite what is there. Native keeps no revision history of copy, but when `update_post` replaces copy, its result tells you what was there before. ## Rules for every edit - A post that is already published is refused with `post_already_published`. `publishedPlatforms` lists the destinations that are live. - Any content change gives the post a new `contentDigest` and makes an earlier approval stale. Read the post again, have it reviewed again and approve it again before you schedule. See [what the digest covers](https://docs.native.no/native-api/approval#content-digest). - An edit does not clear a rejection. Only a new `approve_post` does. - A publishing provider that holds a post for a scheduled time freezes its media. Picture edits are refused with `post_already_scheduled` until you withdraw every held destination with `unschedule_platform`. See [withdraw a destination](https://docs.native.no/native-api/publishing#withdraw-a-destination). - `generate_caption` and `regenerate_post_image` spend the workspace’s AI allowance. When it runs out they fail with `usage_limit_exceeded`, and `resetDate` says when it refills. - After a timeout or a dropped connection, read the post with `view_post` before you retry. A lost response does not prove that nothing changed. - Destinations are uppercase names, such as `INSTAGRAM`. ## Change copy, time or destinations MCP tool `update_post` (preview) Overwrites the caption, the first comment, the intended time or the set of destinations. Send at least one of `text`, `firstComment`, `scheduledAt`, `addPlatforms`, `removePlatforms` or `reddit`. Input: - `id` (string, required): The post id. - `text` (string, optional): The new caption, as plain text. It goes to every destination unless `platform` is set. Shared copy has to fit the shortest destination’s limit: a destination it is too long for keeps its old caption, and `captionResults` says so. - `firstComment` (string | null, optional): The new first comment. `null` clears it. It goes to every destination unless `platform` is set. - `platform` (string, optional): Write `text` and `firstComment` to one existing destination only, such as `LINKEDIN`, or to `all`. It never adds, removes or picks out a destination. Sent without `text` or `firstComment`, it is refused, with two exceptions: the value `all`, and a call that also sends `addPlatforms` or `removePlatforms`. In both cases it is ignored. - `scheduledAt` (ISO 8601 string | null, optional): On a draft, an intended time only: it does not schedule anything. `null` clears it. A time a publishing provider holds cannot be cleared this way. To book or move a post, use [`schedule_post`](https://docs.native.no/native-api/publishing#schedule-a-post). - `addPlatforms` (string[], optional): Destinations to add, at least one. They take the post’s copy and media. Adding `REDDIT` needs `reddit`. On a scheduled post, an added destination is not scheduled by this call. - `removePlatforms` (string[], optional): Destinations to remove, at least one. Must not overlap `addPlatforms`. The destinations you do not name keep their own captions and schedule. Removing every destination is refused. - `reddit` (object, optional): Reddit options. Required when `addPlatforms` includes `REDDIT`. Sent on its own, it rewrites the options of an existing Reddit destination. - `subreddit` (string, required): 1 to 100 characters. - `title` (string, optional): The Reddit post title. Up to 300 characters. Defaults to the caption’s first line. - `flairId` (string, optional): Up to 200 characters. The rules about which fields go together are checked before the tool runs. Breaking one gives you an MCP SDK validation error, not the error envelope. Treat that as a bug in your call. ### What the result says The result is the post as it now reads, with captions as plain text, plus what the call changed. It also carries `view_post`’s preview fields: `authorName`, `authorSubtitle`, `authorAvatarUrl`, `postText`, `imageUrl`, `imageUrls`, `destinations`, `appOrigin` and `appUrl`. Each of those may be absent if reading the post back failed. The rest of the post comes through as Native sends it, so ignore fields you do not use. Output: - `postId` (string, required): The post id. - `contentDigest` (string | null, optional): The post’s digest after the edit. It comes through with the post rather than being declared in the output schema. - `approval` (object | null, optional): The latest review decision, shaped like [the approval object](https://docs.native.no/native-api/approval#approval-object). After a content change, `valid` is `false`. It comes through with the post rather than being declared in the output schema. - `platformResults` (object[], optional): Each destination’s publishing state, as `get_post_status` returns it. May be absent if reading it back failed. - `platformPostId` (string, optional): The id `unschedule_platform` takes. May be absent. - `platform` (string, required): The destination. - `state` (string, required; one of: `published`, `failed`, `stuck`, `scheduled`, `pending`, `unknown`): What this destination is doing. - `scheduledAt` (string | null, optional): This destination’s own time. May be absent. - `url` (string | null, required): Set once the destination is live. - `error` (object | null, required): On a failed destination, `code`, `message` and `retryable`. `null` otherwise. - `publishedUrls` (string[], optional): Live links. May be absent. - `captionResults` (object[], optional): What a caption write did to each destination it targeted. May be absent. - `platform` (string, required): The destination. - `captionUpdated` (boolean, required): The caption was written. - `firstCommentUpdated` (boolean, required): The first comment was written. - `characterCount` (integer, required): The caption’s length. - `characterLimit` (integer, required): This destination’s limit. `0` means no limit is on record. - `skippedReason` (string, optional): Present only when this destination kept its old caption, and says why. - `captionWarning` (string, optional): Some destinations kept their old caption because the new one was too long for them. Written for people. May be absent. - `platformsAdded` (string[], optional): Destinations this call added. May be absent. - `platformsRemoved` (string[], optional): Destinations this call removed. May be absent. - `platformSetWarning` (string, optional): An added destination took a caption that is over its limit. Shorten it before you schedule. May be absent. - `scheduleHeld` (boolean, optional): Present when the post was scheduled before the call. `false` means it is no longer scheduled after the edit, or that Native could not check. Then read `get_post_status`, and call `schedule_post` again if the post is not booked. - `scheduleWarning` (string, optional): Why the schedule did not hold. Written for people. May be absent. - `replaced` (object, optional): Present when the call overwrote content the post already had. This is the only copy Native keeps. - `text` (boolean, required): The caption was overwritten. - `firstComment` (boolean, required): The first comment was overwritten. - `scheduledAt` (boolean, required): The intended time was overwritten. - `previousText` (string, optional): The caption before the call. - `previousFirstComment` (string, optional): The first comment before the call. - `previousScheduledAt` (string, optional): The intended time before the call. The examples on this page use `callNative` from [End-to-end flow](https://docs.native.no/native-api/flow#connect-to-the-gateway). It returns the tool’s payload, or throws a `NativeError` that carries the error’s `code` and `retryable`. Change a caption: ```ts // callNative and NativeError are defined in /docs/native-api/flow. const updated = await callNative('update_post', { id: '5a2f9c71-3e8d-4b60-a7f4-1c9e2d8b6f05', text: 'Autumn hours start on Monday: 7 to 18 on weekdays, 8 to 15 on Saturdays.', }) for (const result of updated.captionResults ?? []) { if (!result.captionUpdated) { console.warn(result.platform + ' kept its old caption: ' + result.skippedReason) } } // The earlier approval no longer covers the post: show it to the reviewer again. const needsReview = updated.approval?.valid === false ``` Response (excerpt): ```json { "postId": "5a2f9c71-3e8d-4b60-a7f4-1c9e2d8b6f05", "contentDigest": "v3.3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d", "approval": { "decision": "APPROVED", "reviewerName": "Kari Nordmann", "reviewerEmail": "kari@example.com", "reason": null, "decidedAt": "2026-10-12T09:14:00.000Z", "approvedDigest": "v3.ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", "valid": false }, "captionResults": [ { "platform": "INSTAGRAM", "captionUpdated": true, "firstCommentUpdated": false, "characterCount": 72, "characterLimit": 2200 } ], "replaced": { "text": true, "firstComment": false, "scheduledAt": false, "previousText": "Autumn hours start on Monday. We open at 7 on weekdays, so the first cinnamon buns are out before your train." }, "appOrigin": "https://app.native.no", "appUrl": "https://app.native.no/api/open-post?id=5a2f9c71-3e8d-4b60-a7f4-1c9e2d8b6f05" } ``` ### Errors | Code | `retryable` | What to do | | --- | --- | --- | | `post_already_published` | `false` | `publishedPlatforms` lists what is live. Take those destinations down with `unpublish_post` if the post has to change, or make a new draft. | | `caption_over_limit` | `false` | No destination accepted the text, so nothing was written. `characterCount` is the length you sent, `characterLimit` the limit to fit and `overLimitPlatforms` the destinations that refused it. Shorten the text, or write the long version to one destination with `platform`. | | `already_scheduled` | `false` | A publishing provider holds the post’s time, so `scheduledAt: null` cannot clear it. `lockedDestinations` lists each held destination as `platform` and `platformPostId`. Withdraw each one with `unschedule_platform`, or move the post with `schedule_post`. | | `platform_not_connected` | `false` | A destination in `addPlatforms` has no live connection. Read `unavailablePlatforms` and `connectedPlatforms`. | | `platform_not_supported` | `false` | Native does not publish to a destination in `addPlatforms`. Read `unsupportedPlatforms` and `supportedPlatforms`. | | `reddit_options_required` | `false` | `addPlatforms` includes `REDDIT`, but `reddit` was not sent. | | `media_required` | `false` | You added a story destination, and the post has no picture or video. Attach one first, or drop the story destination. | | `image_generation_in_progress` | `false` | A picture is still rendering, so the destinations cannot change yet: a destination added now would miss it. Poll `jobId` with `get_generation_job`, then try again. `jobId` can be missing. | | `bad_request` | `false` | Native refused the change, for example because it would remove every destination, or names a destination the post does not have. Fix what `invalidFields` and `fieldErrors` name. | | `upstream_409` | `false` | You tried to remove a destination that a publishing provider is holding. Withdraw it with `unschedule_platform` first. This refusal has no code of its own in the gateway today. | | `platform_scheduled` | `false` | New, in development: the same refusal as `upstream_409` above, under its own code. Withdraw the held destination with `unschedule_platform`, then remove it. Its `platformPostId` is in `get_post_status`. | Any tool can also return the common errors, such as `not_found`, `subscription_required` or `upstream_timeout`. [Errors](https://docs.native.no/native-api/errors) lists every code. ## Rewrite a caption MCP tool `generate_caption` (preview) Native rewrites a caption in the brand’s voice and replaces the current one. It keeps no copy of the caption it replaces, and this result has no `replaced`. If you may want the old caption back, read it with `view_post` first. Input: - `postId` (string, required): The post id. - `instructions` (string, optional): Up to 2,000 characters. Refines the caption the post holds now, so a caption that already meets them comes back nearly unchanged. With `fromBrief`, they steer the fresh text instead. - `platform` (string, optional): Rewrite one destination only, such as `LINKEDIN`, or `all`. That destination is refined from its own current copy, in that platform’s style, and no other caption changes. Without it, the post’s main caption is rewritten and copied to the destinations whose caption was never edited by hand. - `fromBrief` (boolean, optional): Write a fresh caption from the post’s original brief instead of refining the current one. Refused with `bad_request` on a post created without a brief. The result is everything `update_post` returns except `replaced`, plus `generatedCaption`. `captionResults` says exactly which destinations changed, so you can tell a hand-edited caption Native left alone from one it skipped. Output, in addition to update_post’s: - `generatedCaption` (string, optional): The caption Native wrote, as plain text. May be absent. ### Errors | Code | `retryable` | What to do | | --- | --- | --- | | `post_already_published` | `false` | `publishedPlatforms` lists what is live. Make a new draft instead. | | `usage_limit_exceeded` | `false` | The workspace’s AI allowance is used up. `resetDate` says when it refills. | | `ai_generation_failed` | `true` | Writing the caption failed. Try again. | | `bad_request` | `false` | For example `fromBrief: true` on a post created without a brief. Fix what `invalidFields` and `fieldErrors` name. | | `profile_not_ready` | `true` | New, in development: the profile `create_profile` started is not ready. Poll `get_profile` until `onboarding.status` is `ready`. | ## Redraw one picture MCP tool `regenerate_post_image` (preview) Redraws one picture of a post in its slot and leaves the other slides alone. The call returns at once with a `jobId`. The render runs in the background, and you follow it with `get_generation_job`. Do not call it again to find out how a render went: poll its job. While a render runs, a second call is refused with `image_generation_in_progress`. Once the render has ended, a second call starts a new one. Input: - `postId` (string, required): The post id. - `index` (integer, optional): The slide to redraw, from 0 to 99: its position in `imageUrls`. Send exactly one of `index` or `imageId`. `view_post` returns no image ids, so `index` is usually the one to use. - `imageId` (string, optional): The image’s own id, such as an `images[].imageId` from an earlier result of this tool. - `prompt` (string, required): What to change about this slide. 1 to 2,000 characters. Describe the subject, framing, mood and setting the change touches, and write names and facts exactly as they should appear, because the image model invents what the prompt leaves out. Leave out the brand’s styling and the caption. - `referenceImageUrls` (string[], optional): Up to 9 distinct URLs of pictures in the brand’s Native library, to steer the render. The current slide stays the base. Any other URL is refused. The schema allows 10, but Native refuses more than 9 distinct URLs with `bad_request`, because the slide being redrawn takes the tenth place. Output: - `postId` (string, required): The post id. - `images` (object[], required): The post’s slots as they are now. The new picture is not here yet. - `index` (integer, required): The slot’s position, from 0. - `url` (string, required): The picture in the slot now. - `filename` (string, required): The file’s name. - `filetype` (string, optional): Tells a picture from a video. May be absent. - `altText` (string | null, required): The alt text written for the picture, or `null`. - `imageId` (string, required): The image’s own id. - `variants` (object[], required): One per destination. - `platform` (string, required): The destination. - `platformPostId` (string, required): The destination’s own id. - `imageCount` (integer, required): How many pictures the destination carries. - `imageGeneration` (object, required): The render that just started. - `jobId` (string, required): Poll it with `get_generation_job`. - `status` (string, required; one of: `queued`): Always `queued`. ### Aspect ratio The new picture is drawn at the ratio of the first real destination holding that slot. That is 9:16 for `INSTAGRAM_STORIES`, `FACEBOOK_STORIES` and `TIKTOK`, and 4:5 for `INSTAGRAM`, `FACEBOOK`, `LINKEDIN` and `TWITTER`. 4:5 is also the default. On a stories-only post, the new picture is 9:16. ### Workspaces that render from templates > **In development.** On a workspace that renders posts from design templates, `regenerate_post_image` is refused with `template_rendering`. Use `generate_image` instead: it redraws the whole design, so the slide text stays exact. It replaces all of the post’s pictures, not one slide. A template render takes a few minutes. `generate_image` with `saveToLibrary` is refused there with the same code. These pages do not cover `generate_image` yet: `tools/list` has its schema. ### Errors | Code | `retryable` | What to do | | --- | --- | --- | | `post_already_published` | `false` | `publishedPlatforms` lists what is live. Make a new draft instead. | | `post_already_scheduled` | `false` | A publishing provider holds the post, so its media is frozen. `lockedDestinations` lists every held destination with its `platformPostId`. Call `unschedule_platform` once per entry, for all of them, then redraw and schedule again. Retrying before that fails the same way. | | `image_generation_in_progress` | `false` | A render is already running on this post. Poll its `jobId` with `get_generation_job` instead of calling again. `jobId` can be missing. | | `usage_limit_exceeded` | `false` | The workspace’s AI allowance is used up. `resetDate` says when it refills. | | `bad_request` | `false` | For example a reference URL from outside the brand’s Native library, or more than 9 distinct reference URLs. Fix what `invalidFields` and `fieldErrors` name. | | `profile_not_ready` | `true` | New, in development: the profile `create_profile` started is not ready. Poll `get_profile` until `onboarding.status` is `ready`. | | `template_rendering` | `false` | New, in development: the workspace renders posts from design templates. Use `generate_image` instead. | ## Follow a render MCP tool `get_generation_job` (preview) Reports how a picture render is going, and how it ended. By default the call waits up to about 24 seconds for the render to finish before it answers. Input: - `jobId` (string, required): From `imageGeneration.jobId`, or from an `image_generation_in_progress` error. - `wait` (boolean, optional; default: `true`): With `true`, the call holds for up to about 24 seconds until the render settles. With `false`, it returns the current state at once. Output: - `jobId` (string, required): The render. - `kind` (string, required; one of: `image`): Always `image`. - `operation` (string, required): What started the render, for example `POST_REGENERATE` for `regenerate_post_image`. Treat it as an open set. - `status` (string, required; one of: `queued`, `generating`, `finalizing`, `completed`, `failed`): Where the render is. `completed` and `failed` are final. - `postId` (string | null, required): The post the render is for. - `brandId` (string, required): The brand. - `companyId` (string, required): The client’s workspace. - `createdAt` (ISO 8601 string, required): When the render was asked for. - `startedAt` (ISO 8601 string | null, required): When it started. - `completedAt` (ISO 8601 string | null, required): When it ended. - `error` (object | null, required): Why the render failed. `null` unless `status` is `failed`. - `code` (string, required; one of: `content_rejected`, `provider_timeout`, `provider_unavailable`, `quota_exhausted`, `generation_failed`): Branch on this. See [render failures](https://docs.native.no/native-api/editing-posts#render-failures). - `message` (string, required): Written for people. It can change at any time. - `retryable` (boolean, required): Whether a new render could succeed without a change. - `outputs` (object[], required): One per requested slide, so you can see a render where one slide failed. - `index` (integer, required): The slide, from 0. - `status` (string, required; one of: `pending`, `completed`, `failed`): Where this slide is. - `imageUrl` (string | null, required): The new picture, once it lands. - `error` (object | null, required): Why this slide failed, in the same shape as `error`. - `imageUrls` (string[], required): The pictures this render produced. Empty until they land. - `libraryImageIds` (string[], optional): Library ids for the produced pictures, only for a render started with `saveToLibrary` on `generate_image`. Empty otherwise. May be absent. - `post` (object, optional): Present once the render has settled and the post could be read: the post with its new pictures. It carries `view_post`’s preview fields, `appOrigin` and `appUrl`, but no `contentDigest` or `approval`. - `authorName` (string, required): As in `view_post`. - `authorSubtitle` (string | null, required): As in `view_post`. - `authorAvatarUrl` (string | null, required): As in `view_post`. - `postText` (string, required): As in `view_post`. - `imageUrl` (string | null, required): As in `view_post`. - `imageUrls` (string[], required): As in `view_post`. - `destinations` (object[], required): As in `view_post`. See [the destination object](https://docs.native.no/native-api/generating-posts#destination-object). - `appOrigin` (string, required): The Native app’s address. - `appUrl` (string, required): The post in the Native app. Errors: `not_found` when no render has that id, and `forbidden` for a render in a workspace you cannot reach, plus the other common errors. ### Render failures A failed render has ended, so starting a new one is safe. Do it only when `retryable` is `true`, or after you have changed what the code points at. | Code | `retryable` | What it means | | --- | --- | --- | | `content_rejected` | `false` | The image provider refused the request, for example on content policy. Change the prompt before you try again. | | `provider_timeout` | `true` | The image provider took too long. Start a new render. | | `provider_unavailable` | `true` | The image provider was busy or down. Start a new render after a pause. | | `quota_exhausted` | `false` | The image provider refused for quota or billing reasons. A new render will not help. Contact Native if it persists. | | `generation_failed` | `true` | Any other failure. A new render may succeed. | ### Polling - Call with the default `wait: true`. Each call already waits about 24 seconds, so call again straight away while `status` is `queued`, `generating` or `finalizing`. - Or call with `wait: false` every 5 seconds. - Stop when `status` is `completed` or `failed`. - A template render takes a few minutes, so expect several calls. - Never call `regenerate_post_image` again to retry a render that is still running. Redraw a slide and wait for it: ```ts async function redrawSlide(postId: string, index: number, prompt: string) { let jobId: string try { const started = await callNative('regenerate_post_image', { postId, index, prompt }) jobId = started.imageGeneration.jobId } catch (err) { // A render is already running on this post: follow that one instead. if (!(err instanceof NativeError) || err.code !== 'image_generation_in_progress') throw err const runningJobId = (err.body as { jobId?: string }).jobId if (!runningJobId) throw err jobId = runningJobId } // Each call waits up to about 24 s, so 20 calls cover about 8 minutes. for (let attempt = 0; attempt < 20; attempt++) { const job = await callNative('get_generation_job', { jobId }) if (job.status === 'completed' || job.status === 'failed') return job } throw new Error('Render ' + jobId + ' is still running') } const job = await redrawSlide( '5a2f9c71-3e8d-4b60-a7f4-1c9e2d8b6f05', 0, 'The same counter and buns in early morning light, with the sign in the window reading "Open from 7"', ) if (job.status === 'failed') console.warn(job.error?.code, job.error?.retryable) // The post has a new picture and a new contentDigest. Read it with view_post before review. ``` Response: ```json { "jobId": "a41c7e2d-9b8f-4d36-8e15-0f2a6b9c3d70", "kind": "image", "operation": "POST_REGENERATE", "status": "completed", "postId": "5a2f9c71-3e8d-4b60-a7f4-1c9e2d8b6f05", "brandId": "8f0c2b7e-1d3a-4c5f-9e8b-7a6d5c4b3a21", "companyId": "c2a7e9d1-5b3f-4e8a-a1c6-9d0b2f4e7a58", "createdAt": "2026-10-12T09:20:00.000Z", "startedAt": "2026-10-12T09:20:02.000Z", "completedAt": "2026-10-12T09:20:41.000Z", "error": null, "outputs": [ { "index": 0, "status": "completed", "imageUrl": "https://storage.googleapis.com/example-bucket/posts/5a2f/slide-1-v2.png", "error": null } ], "imageUrls": ["https://storage.googleapis.com/example-bucket/posts/5a2f/slide-1-v2.png"], "libraryImageIds": [], "post": { "authorName": "Solsiden Bakery", "authorSubtitle": "Draft", "authorAvatarUrl": "https://storage.googleapis.com/example-bucket/logos/solsiden.png", "postText": "Autumn hours start on Monday: 7 to 18 on weekdays, 8 to 15 on Saturdays.", "imageUrl": "https://storage.googleapis.com/example-bucket/posts/5a2f/slide-1-v2.png", "imageUrls": ["https://storage.googleapis.com/example-bucket/posts/5a2f/slide-1-v2.png"], "destinations": [ { "platform": "INSTAGRAM", "text": "Autumn hours start on Monday: 7 to 18 on weekdays, 8 to 15 on Saturdays.", "firstComment": null, "title": null, "imageUrls": ["https://storage.googleapis.com/example-bucket/posts/5a2f/slide-1-v2.png"], "thumbnailUrl": null, "characterCount": 72, "characterLimit": 2200, "publishingOptions": null } ], "appOrigin": "https://app.native.no", "appUrl": "https://app.native.no/api/open-post?id=5a2f9c71-3e8d-4b60-a7f4-1c9e2d8b6f05" } } ``` ## After an edit 1. Read the post again with `view_post`. A render’s `post` has no `contentDigest`, so you need this read. 2. Show the reviewer every destination from that read. 3. Call `approve_post` with that read’s `contentDigest`, or `reject_post` with a reason. See [Review and approval](https://docs.native.no/native-api/approval). 4. Schedule or publish only after that. A stale approval is refused at scheduling with `approval_stale`.