Native APITools
PreviewEditing 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.publishedPlatformslists the destinations that are live. - Any content change gives the post a new
contentDigestand 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_postdoes. - A publishing provider that holds a post for a scheduled time freezes its media. Picture edits are refused with
post_already_scheduleduntil you withdraw every held destination withunschedule_platform. See withdraw a destination. generate_captionandregenerate_post_imagespend the workspace’s AI allowance. When it runs out they fail withusage_limit_exceeded, andresetDatesays when it refills.- After a timeout or a dropped connection, read the post with
view_postbefore you retry. A lost response does not prove that nothing changed. - Destinations are uppercase names, such as
INSTAGRAM.
Change copy, time or destinations
#update_postPreviewOverwrites 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
idstringRequiredThe post id.
textstringOptionalThe new caption, as plain text. It goes to every destination unless
platformis set. Shared copy has to fit the shortest destination’s limit: a destination it is too long for keeps its old caption, andcaptionResultssays so.firstCommentstring | nullOptionalThe new first comment.
nullclears it. It goes to every destination unlessplatformis set.platformstringOptionalWrite
textandfirstCommentto one existing destination only, such asLINKEDIN, or toall. It never adds, removes or picks out a destination. Sent withouttextorfirstComment, it is refused, with two exceptions: the valueall, and a call that also sendsaddPlatformsorremovePlatforms. In both cases it is ignored.scheduledAtISO 8601 string | nullOptionalOn a draft, an intended time only: it does not schedule anything.
nullclears it. A time a publishing provider holds cannot be cleared this way. To book or move a post, useschedule_post.addPlatformsstring[]OptionalDestinations to add, at least one. They take the post’s copy and media. Adding
REDDITneedsreddit. On a scheduled post, an added destination is not scheduled by this call.removePlatformsstring[]OptionalDestinations 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.redditobjectOptionalReddit options. Required when
addPlatformsincludesREDDIT. Sent on its own, it rewrites the options of an existing Reddit destination.subredditstringRequired1 to 100 characters.
titlestringOptionalThe Reddit post title. Up to 300 characters. Defaults to the caption’s first line.
flairIdstringOptionalUp 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
postIdstringRequiredThe post id.
contentDigeststring | nullOptionalThe post’s digest after the edit. It comes through with the post rather than being declared in the output schema.
approvalobject | nullOptionalThe latest review decision, shaped like the approval object. After a content change,
validisfalse. It comes through with the post rather than being declared in the output schema.platformResultsobject[]OptionalEach destination’s publishing state, as
get_post_statusreturns it. May be absent if reading it back failed.platformPostIdstringOptionalThe id
unschedule_platformtakes. May be absent.platformstringRequiredThe destination.
statestringRequiredWhat this destination is doing.
One of:
published,failed,stuck,scheduled,pending,unknownscheduledAtstring | nullOptionalThis destination’s own time. May be absent.
urlstring | nullRequiredSet once the destination is live.
errorobject | nullRequiredOn a failed destination,
code,messageandretryable.nullotherwise.
publishedUrlsstring[]OptionalLive links. May be absent.
captionResultsobject[]OptionalWhat a caption write did to each destination it targeted. May be absent.
platformstringRequiredThe destination.
captionUpdatedbooleanRequiredThe caption was written.
firstCommentUpdatedbooleanRequiredThe first comment was written.
characterCountintegerRequiredThe caption’s length.
characterLimitintegerRequiredThis destination’s limit.
0means no limit is on record.skippedReasonstringOptionalPresent only when this destination kept its old caption, and says why.
captionWarningstringOptionalSome destinations kept their old caption because the new one was too long for them. Written for people. May be absent.
platformsAddedstring[]OptionalDestinations this call added. May be absent.
platformsRemovedstring[]OptionalDestinations this call removed. May be absent.
platformSetWarningstringOptionalAn added destination took a caption that is over its limit. Shorten it before you schedule. May be absent.
scheduleHeldbooleanOptionalPresent when the post was scheduled before the call.
falsemeans it is no longer scheduled after the edit, or that Native could not check. Then readget_post_status, and callschedule_postagain if the post is not booked.scheduleWarningstringOptionalWhy the schedule did not hold. Written for people. May be absent.
replacedobjectOptionalPresent when the call overwrote content the post already had. This is the only copy Native keeps.
textbooleanRequiredThe caption was overwritten.
firstCommentbooleanRequiredThe first comment was overwritten.
scheduledAtbooleanRequiredThe intended time was overwritten.
previousTextstringOptionalThe caption before the call.
previousFirstCommentstringOptionalThe first comment before the call.
previousScheduledAtstringOptionalThe 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 === falseResponse (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
#| 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 lists every code.
Rewrite a caption
#generate_captionPreviewNative 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
postIdstringRequiredThe post id.
instructionsstringOptionalUp 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.platformstringOptionalRewrite one destination only, such as
LINKEDIN, orall. 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.fromBriefbooleanOptionalWrite a fresh caption from the post’s original brief instead of refining the current one. Refused with
bad_requeston 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
generatedCaptionstringOptionalThe 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
#regenerate_post_imagePreviewRedraws 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
postIdstringRequiredThe post id.
indexintegerOptionalThe slide to redraw, from 0 to 99: its position in
imageUrls. Send exactly one ofindexorimageId.view_postreturns no image ids, soindexis usually the one to use.imageIdstringOptionalThe image’s own id, such as an
images[].imageIdfrom an earlier result of this tool.promptstringRequiredWhat 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[]OptionalUp 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
postIdstringRequiredThe post id.
imagesobject[]RequiredThe post’s slots as they are now. The new picture is not here yet.
indexintegerRequiredThe slot’s position, from 0.
urlstringRequiredThe picture in the slot now.
filenamestringRequiredThe file’s name.
filetypestringOptionalTells a picture from a video. May be absent.
altTextstring | nullRequiredThe alt text written for the picture, or
null.imageIdstringRequiredThe image’s own id.
variantsobject[]RequiredOne per destination.
platformstringRequiredThe destination.
platformPostIdstringRequiredThe destination’s own id.
imageCountintegerRequiredHow many pictures the destination carries.
imageGenerationobjectRequiredThe render that just started.
jobIdstringRequiredPoll it with
get_generation_job.statusstringRequiredAlways
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
#| 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
#get_generation_jobPreviewReports 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
jobIdstringRequiredFrom
imageGeneration.jobId, or from animage_generation_in_progresserror.waitbooleanOptionalWith
true, the call holds for up to about 24 seconds until the render settles. Withfalse, it returns the current state at once.Default:
true
Output
jobIdstringRequiredThe render.
kindstringRequiredAlways
image.One of:
imageoperationstringRequiredWhat started the render, for example
POST_REGENERATEforregenerate_post_image. Treat it as an open set.statusstringRequiredWhere the render is.
completedandfailedare final.One of:
queued,generating,finalizing,completed,failedpostIdstring | nullRequiredThe post the render is for.
brandIdstringRequiredThe brand.
companyIdstringRequiredThe client’s workspace.
createdAtISO 8601 stringRequiredWhen the render was asked for.
startedAtISO 8601 string | nullRequiredWhen it started.
completedAtISO 8601 string | nullRequiredWhen it ended.
errorobject | nullRequiredWhy the render failed.
nullunlessstatusisfailed.codestringRequiredBranch on this. See render failures.
One of:
content_rejected,provider_timeout,provider_unavailable,quota_exhausted,generation_failedmessagestringRequiredWritten for people. It can change at any time.
retryablebooleanRequiredWhether a new render could succeed without a change.
outputsobject[]RequiredOne per requested slide, so you can see a render where one slide failed.
indexintegerRequiredThe slide, from 0.
statusstringRequiredWhere this slide is.
One of:
pending,completed,failedimageUrlstring | nullRequiredThe new picture, once it lands.
errorobject | nullRequiredWhy this slide failed, in the same shape as
error.
imageUrlsstring[]RequiredThe pictures this render produced. Empty until they land.
libraryImageIdsstring[]OptionalLibrary ids for the produced pictures, only for a render started with
saveToLibraryongenerate_image. Empty otherwise. May be absent.postobjectOptionalPresent once the render has settled and the post could be read: the post with its new pictures. It carries
view_post’s preview fields,appOriginandappUrl, but nocontentDigestorapproval.authorNamestringRequiredAs in
view_post.authorSubtitlestring | nullRequiredAs in
view_post.authorAvatarUrlstring | nullRequiredAs in
view_post.postTextstringRequiredAs in
view_post.imageUrlstring | nullRequiredAs in
view_post.imageUrlsstring[]RequiredAs in
view_post.destinationsobject[]RequiredAs in
view_post. See the destination object.appOriginstringRequiredThe Native app’s address.
appUrlstringRequiredThe 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 whilestatusisqueued,generatingorfinalizing. - Or call with
wait: falseevery 5 seconds. - Stop when
statusiscompletedorfailed. - A template render takes a few minutes, so expect several calls.
- Never call
regenerate_post_imageagain 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
#- Read the post again with
view_post. A render’sposthas nocontentDigest, so you need this read. - Show the reviewer every destination from that read.
- Call
approve_postwith that read’scontentDigest, orreject_postwith a reason. See Review and approval. - 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.