Native API· Editing posts

Native APITools

Preview

Editing posts

Change a post’s caption, first comment, destinations or a single picture, and follow a picture render until it lands.

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.
  • 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.
  • 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

#
TOOLupdate_postPreview

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

  • idstringRequired

    The post id.

  • textstringOptional

    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.

  • firstCommentstring | nullOptional

    The new first comment. null clears it. It goes to every destination unless platform is set.

  • platformstringOptional

    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.

  • scheduledAtISO 8601 string | nullOptional

    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.

  • addPlatformsstring[]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.

  • removePlatformsstring[]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.

  • redditobjectOptional

    Reddit options. Required when addPlatforms includes REDDIT. Sent on its own, it rewrites the options of an existing Reddit destination.

    • subredditstringRequired

      1 to 100 characters.

    • titlestringOptional

      The Reddit post title. Up to 300 characters. Defaults to the caption’s first line.

    • flairIdstringOptional

      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

  • postIdstringRequired

    The post id.

  • contentDigeststring | nullOptional

    The post’s digest after the edit. It comes through with the post rather than being declared in the output schema.

  • approvalobject | nullOptional

    The latest review decision, shaped like the approval object. After a content change, valid is false. It comes through with the post rather than being declared in the output schema.

  • platformResultsobject[]Optional

    Each destination’s publishing state, as get_post_status returns it. May be absent if reading it back failed.

    • platformPostIdstringOptional

      The id unschedule_platform takes. May be absent.

    • platformstringRequired

      The destination.

    • statestringRequired

      What this destination is doing.

      One of: published, failed, stuck, scheduled, pending, unknown

    • scheduledAtstring | nullOptional

      This destination’s own time. May be absent.

    • urlstring | nullRequired

      Set once the destination is live.

    • errorobject | nullRequired

      On a failed destination, code, message and retryable. null otherwise.

  • publishedUrlsstring[]Optional

    Live links. May be absent.

  • captionResultsobject[]Optional

    What a caption write did to each destination it targeted. May be absent.

    • platformstringRequired

      The destination.

    • captionUpdatedbooleanRequired

      The caption was written.

    • firstCommentUpdatedbooleanRequired

      The first comment was written.

    • characterCountintegerRequired

      The caption’s length.

    • characterLimitintegerRequired

      This destination’s limit. 0 means no limit is on record.

    • skippedReasonstringOptional

      Present only when this destination kept its old caption, and says why.

  • captionWarningstringOptional

    Some destinations kept their old caption because the new one was too long for them. Written for people. May be absent.

  • platformsAddedstring[]Optional

    Destinations this call added. May be absent.

  • platformsRemovedstring[]Optional

    Destinations this call removed. May be absent.

  • platformSetWarningstringOptional

    An added destination took a caption that is over its limit. Shorten it before you schedule. May be absent.

  • scheduleHeldbooleanOptional

    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.

  • scheduleWarningstringOptional

    Why the schedule did not hold. Written for people. May be absent.

  • replacedobjectOptional

    Present when the call overwrote content the post already had. This is the only copy Native keeps.

    • textbooleanRequired

      The caption was overwritten.

    • firstCommentbooleanRequired

      The first comment was overwritten.

    • scheduledAtbooleanRequired

      The intended time was overwritten.

    • previousTextstringOptional

      The caption before the call.

    • previousFirstCommentstringOptional

      The first comment before the call.

    • previousScheduledAtstringOptional

      The intended time before the call.

The examples on this page use callNative from End-to-end flow. It returns the tool’s payload, or throws a NativeError that carries the error’s code and retryable.

Change a caption

// 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)

{
  "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

#
CoderetryableWhat to do
post_already_publishedfalsepublishedPlatforms lists what is live. Take those destinations down with unpublish_post if the post has to change, or make a new draft.
caption_over_limitfalseNo 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_scheduledfalseA 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_connectedfalseA destination in addPlatforms has no live connection. Read unavailablePlatforms and connectedPlatforms.
platform_not_supportedfalseNative does not publish to a destination in addPlatforms. Read unsupportedPlatforms and supportedPlatforms.
reddit_options_requiredfalseaddPlatforms includes REDDIT, but reddit was not sent.
media_requiredfalseYou added a story destination, and the post has no picture or video. Attach one first, or drop the story destination.
image_generation_in_progressfalseA 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_requestfalseNative 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_409falseYou 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_scheduledfalseNew, 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 lists every code.

Rewrite a caption

#
TOOLgenerate_captionPreview

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

  • postIdstringRequired

    The post id.

  • instructionsstringOptional

    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.

  • platformstringOptional

    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.

  • fromBriefbooleanOptional

    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

  • generatedCaptionstringOptional

    The caption Native wrote, as plain text. May be absent.

Errors

#
CoderetryableWhat to do
post_already_publishedfalsepublishedPlatforms lists what is live. Make a new draft instead.
usage_limit_exceededfalseThe workspace’s AI allowance is used up. resetDate says when it refills.
ai_generation_failedtrueWriting the caption failed. Try again.
bad_requestfalseFor example fromBrief: true on a post created without a brief. Fix what invalidFields and fieldErrors name.
profile_not_readytrueNew, in development: the profile create_profile started is not ready. Poll get_profile until onboarding.status is ready.

Redraw one picture

#
TOOLregenerate_post_imagePreview

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

  • postIdstringRequired

    The post id.

  • indexintegerOptional

    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.

  • imageIdstringOptional

    The image’s own id, such as an images[].imageId from an earlier result of this tool.

  • promptstringRequired

    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.

  • referenceImageUrlsstring[]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

  • postIdstringRequired

    The post id.

  • imagesobject[]Required

    The post’s slots as they are now. The new picture is not here yet.

    • indexintegerRequired

      The slot’s position, from 0.

    • urlstringRequired

      The picture in the slot now.

    • filenamestringRequired

      The file’s name.

    • filetypestringOptional

      Tells a picture from a video. May be absent.

    • altTextstring | nullRequired

      The alt text written for the picture, or null.

    • imageIdstringRequired

      The image’s own id.

  • variantsobject[]Required

    One per destination.

    • platformstringRequired

      The destination.

    • platformPostIdstringRequired

      The destination’s own id.

    • imageCountintegerRequired

      How many pictures the destination carries.

  • imageGenerationobjectRequired

    The render that just started.

    • jobIdstringRequired

      Poll it with get_generation_job.

    • statusstringRequired

      Always queued.

      One of: 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

#

Errors

#
CoderetryableWhat to do
post_already_publishedfalsepublishedPlatforms lists what is live. Make a new draft instead.
post_already_scheduledfalseA 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_progressfalseA render is already running on this post. Poll its jobId with get_generation_job instead of calling again. jobId can be missing.
usage_limit_exceededfalseThe workspace’s AI allowance is used up. resetDate says when it refills.
bad_requestfalseFor 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_readytrueNew, in development: the profile create_profile started is not ready. Poll get_profile until onboarding.status is ready.
template_renderingfalseNew, in development: the workspace renders posts from design templates. Use generate_image instead.

Follow a render

#
TOOLget_generation_jobPreview

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

  • jobIdstringRequired

    From imageGeneration.jobId, or from an image_generation_in_progress error.

  • waitbooleanOptional

    With true, the call holds for up to about 24 seconds until the render settles. With false, it returns the current state at once.

    Default: true

Output

  • jobIdstringRequired

    The render.

  • kindstringRequired

    Always image.

    One of: image

  • operationstringRequired

    What started the render, for example POST_REGENERATE for regenerate_post_image. Treat it as an open set.

  • statusstringRequired

    Where the render is. completed and failed are final.

    One of: queued, generating, finalizing, completed, failed

  • postIdstring | nullRequired

    The post the render is for.

  • brandIdstringRequired

    The brand.

  • companyIdstringRequired

    The client’s workspace.

  • createdAtISO 8601 stringRequired

    When the render was asked for.

  • startedAtISO 8601 string | nullRequired

    When it started.

  • completedAtISO 8601 string | nullRequired

    When it ended.

  • errorobject | nullRequired

    Why the render failed. null unless status is failed.

    • codestringRequired

      Branch on this. See render failures.

      One of: content_rejected, provider_timeout, provider_unavailable, quota_exhausted, generation_failed

    • messagestringRequired

      Written for people. It can change at any time.

    • retryablebooleanRequired

      Whether a new render could succeed without a change.

  • outputsobject[]Required

    One per requested slide, so you can see a render where one slide failed.

    • indexintegerRequired

      The slide, from 0.

    • statusstringRequired

      Where this slide is.

      One of: pending, completed, failed

    • imageUrlstring | nullRequired

      The new picture, once it lands.

    • errorobject | nullRequired

      Why this slide failed, in the same shape as error.

  • imageUrlsstring[]Required

    The pictures this render produced. Empty until they land.

  • libraryImageIdsstring[]Optional

    Library ids for the produced pictures, only for a render started with saveToLibrary on generate_image. Empty otherwise. May be absent.

  • postobjectOptional

    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.

    • authorNamestringRequired

      As in view_post.

    • authorSubtitlestring | nullRequired

      As in view_post.

    • authorAvatarUrlstring | nullRequired

      As in view_post.

    • postTextstringRequired

      As in view_post.

    • imageUrlstring | nullRequired

      As in view_post.

    • imageUrlsstring[]Required

      As in view_post.

    • destinationsobject[]Required

      As in view_post. See the destination object.

    • appOriginstringRequired

      The Native app’s address.

    • appUrlstringRequired

      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.

CoderetryableWhat it means
content_rejectedfalseThe image provider refused the request, for example on content policy. Change the prompt before you try again.
provider_timeouttrueThe image provider took too long. Start a new render.
provider_unavailabletrueThe image provider was busy or down. Start a new render after a pause.
quota_exhaustedfalseThe image provider refused for quota or billing reasons. A new render will not help. Contact Native if it persists.
generation_failedtrueAny 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

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

{
  "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.
  4. Schedule or publish only after that. A stale approval is refused at scheduling with approval_stale.

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.

Contact sales