Native APITools
PreviewGenerating posts
Write up to 20 draft posts from one brief, follow the batch until it settles, and read each post exactly as it would publish.
Generation is always asynchronous. create_posts takes one brief and answers at once with a batchId. Native writes the posts in the background, each from a different angle and with copy for each destination. You poll the batch until it is done, then show each post to your client.
How it works
#- Start a batch with
create_postsand store thebatchId. - Poll
get_post_batch_statusuntilsettledistrueandimagesPendingCountis0. - Read each created post. Once the item fields in development ship, each created item carries its destinations, pictures,
contentDigestandapproval. Until then, callview_postfor eachpostId. - Show the reviewer what you read. Then call
approve_postwith that read’scontentDigest, orreject_postwith a reason. See Review and approval. - To change a post before review, see Editing posts.
A post is a draft until you schedule or publish it. Nothing on this page sends anything to a social platform.
Start a batch
#create_postsPreviewCreates 1 to 20 draft posts from one brief, as one batch. Each post takes a different angle on the brief and gets copy written for each of its destinations. The call returns before any post exists: you get a batchId and one pending slot per post.
Input
brandIdstringRequiredThe client’s brand.
briefstringRequiredWhat the posts are about, in plain words. 1 to 4,000 characters.
countintegerRequiredHow many posts to write, from 1 to 20.
mediaIdsstring[]OptionalUp to 10 brand library image ids. Every post in the batch gets the same pictures. Cannot be combined with
includeImage: true.platformsstring[]OptionalDestinations to add to every post in the batch, as uppercase names such as
INSTAGRAM. Each post also keeps the brand’s default destination, when the brand has one. Leave it out and the brand’s defaults apply. Each item’splatformsinget_post_batch_statusshows where a post ended up.scheduledAtISO 8601 stringOptionalAn intended publishing time, recorded on each post. It does not schedule anything. A time with a numeric offset is converted to UTC.
includeImagebooleanOptionalRender a picture for each post. On by default when you send no
mediaIds.truetogether withmediaIdsis refused, because the render would replace the pictures you sent.
Output
batchIdstringRequiredPass it to
get_post_batch_status.brandIdstringRequiredThe brand the batch is for.
companyIdstringRequiredThe client’s workspace.
statusstringRequiredqueuedwhen the batch is created.requestedCountintegerRequiredThe
countyou asked for.includeImagebooleanOptionalWhether Native will render a picture for each post, once the default is applied. May be absent.
itemsobject[]RequiredOne slot per requested post.
indexintegerRequiredThe slot’s position, from 0.
statusstringRequiredpendingat first.
connectionWarningstringOptionalPresent only when the brand has no connected channel. The posts are written with provisional destinations, and none of them can be scheduled until the client connects an account. The text is written for people.
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.
Start a batch
// callNative and NativeError are defined in /docs/native-api/flow.
// callNative waits up to 150 s, longer than the gateway's 120 s.
const batch = await callNative('create_posts', {
brandId: '8f0c2b7e-1d3a-4c5f-9e8b-7a6d5c4b3a21',
brief: 'Our autumn opening hours start on Monday. We open at 7 on weekdays.',
count: 2,
platforms: ['INSTAGRAM', 'FACEBOOK'],
})
// Store this before anything else: no tool finds a batch without it.
const batchId: string = batch.batchIdResponse
{
"batchId": "6b1f3c2a-8d4e-4f7a-9c1b-2e5d7a9f0c34",
"brandId": "8f0c2b7e-1d3a-4c5f-9e8b-7a6d5c4b3a21",
"companyId": "c2a7e9d1-5b3f-4e8a-a1c6-9d0b2f4e7a58",
"status": "queued",
"requestedCount": 2,
"includeImage": true,
"items": [
{ "index": 0, "status": "pending" },
{ "index": 1, "status": "pending" }
]
}Errors
#These refusals happen before anything is generated, so no batch was created. A post that fails later is reported on its own item in get_post_batch_status.
| Code | retryable | What to do |
|---|---|---|
bad_request | false | Fix what invalidFields and fieldErrors name, then send the call again. |
platform_not_connected | false | A destination you named has no live connection. unavailablePlatforms lists those destinations and connectedPlatforms what is connected. Drop them, or have the client connect the account. |
platform_not_supported | false | Native does not publish to a destination you named. unsupportedPlatforms lists it and supportedPlatforms lists what you can use. |
reddit_options_required | false | A REDDIT destination needs Reddit options, and create_posts takes none. Leave REDDIT out of the batch. Add it to a post afterwards with update_post and its reddit options. |
media_required | false | Every destination is a story, and you sent no mediaIds. A picture Native would render does not count. Send mediaIds, or add a feed destination. |
usage_limit_exceeded | false | The workspace’s AI allowance is used up. resetDate says when it refills. |
ai_generation_failed | true | Generation failed before the batch started. Send the call again. |
profile_not_ready | true | New, in development: the profile create_profile started is not ready. Poll get_profile until onboarding.status is ready. If it is failed, first retry the build with create_profile and the same externalId. See Profiles. |
Any tool can also return the common errors, such as not_found, subscription_required or upstream_timeout. Errors lists every code.
Resending a batch
#A resent create_posts call is a new batch. The gateway does not deduplicate it, and you cannot send your own idempotency key yet. If a call times out or the connection drops, Native may still have started the batch, and a second call can give your client two sets of drafts. No tool finds a batch without its batchId, so store it the moment you have it.
Follow the batch
#get_post_batch_statusPreviewReads a batch and changes nothing, so you can call it as often as the polling guidance below allows.
Input
batchIdstringRequiredFrom
create_posts.
Output
batchIdstringRequiredThe batch.
brandIdstringRequiredThe brand the batch is for.
companyIdstringRequiredThe client’s workspace.
statusstringRequiredWhere the batch is. Branch on
settled, not on this.One of:
queued,generating,partial,ready,failedsettledbooleanRequiredCopy generation has finished:
trueoncestatusisready,partialorfailed. It does not mean every post was created, or that every picture has finished.requestedCountintegerRequiredThe
countyou asked for.createdCountintegerRequiredSlots that became posts.
failedCountintegerRequiredSlots that ended without a post. Counts both
failedandskippeditems.pendingCountintegerRequiredSlots still
pendingorgenerating.imagesPendingCountintegerOptionalCreated posts whose picture is still rendering. May be absent.
warningsstring[]OptionalNotes on the batch as a whole, such as posts with similar copy or a destination that needs media. They are written for people, so show them rather than branch on them. May be absent.
errorstring | nullOptionalWhy the batch failed, when it failed as a whole. May be absent.
itemsobject[]RequiredOne per slot, in order.
indexintegerRequiredThe slot’s position, from 0.
statusstringRequiredskippedmeans the batch stopped before this slot, for example because the workspace’s AI allowance ran out.errorsays why.One of:
pending,generating,created,failed,skippedpostIdstring | nullRequiredSet once the slot is
created.titlestring | nullRequiredThe post’s title.
platformsstring[]RequiredThe post’s destinations.
previewstring | nullOptionalHow the post opens, so you can check that the posts differ. May be absent.
hasMediabooleanOptionalThe post has at least one picture or video. May be absent.
imagePendingbooleanOptionalThe post exists and its copy is done, but its picture is still rendering. May be absent.
mediaRequiredForstring[]OptionalDestinations that cannot publish this post until it has media, such as a story. May be absent.
errorobject | nullRequiredWhy the slot did not become a post.
nullotherwise.codestringRequiredBranch on this. Treat a code you do not know by its
retryableflag.One of:
usage_limit_exceeded,ai_generation_failed,platform_not_connected,platform_not_supported,reddit_options_required,media_required,bad_request,internal_errormessagestringRequiredWritten by Native for people. It can change at any time.
retryablebooleanRequiredWhether the same request, unchanged, could succeed.
appUrlstringOptionalThe post in the Native app. Only on created slots.
destinationsobject[]OptionalNew, in development: each destination exactly as
view_postreturns it. See the destination object. Empty on a slot that holds no post.imageUrlsstring[]OptionalNew, in development: every slide, in order. Empty on a slot that holds no post.
contentDigeststring | nullOptionalNew, in development: the same value
view_postreturns. It changes when a pending picture lands, so approve only onceimagePendingisfalse.nullon a slot that holds no post.approvalobject | nullOptionalNew, in development: the latest review decision, shaped like the approval object.
nullwhen there is none.
createdAtISO 8601 stringOptionalWhen the batch was created. May be absent.
updatedAtISO 8601 stringOptionalWhen the batch last changed. May be absent.
appOriginstringRequiredThe Native app’s address, such as
https://app.native.no.
Item errors
#A slot that failed or was skipped carries an error with its own retryable flag. No tool retries a single slot. To make up for failed slots, start a new batch for the number you are missing.
| Code | What it means |
|---|---|
usage_limit_exceeded | The workspace’s AI allowance ran out during the batch. |
ai_generation_failed | Writing this post failed. |
platform_not_connected | A destination for this post has no live connection. |
platform_not_supported | Native does not publish to a destination for this post. |
reddit_options_required | A REDDIT destination had no Reddit options. |
media_required | Every destination is a story, and the post has no media. |
bad_request | Native refused the post as invalid. |
internal_error | Something failed inside Native. |
Polling
#- Poll every 5 seconds for the first minute, then every 15 seconds, backing off to at most 30 seconds.
- Stop when
settledistrueandimagesPendingCountis0.settledcovers copy only: pictures can still be rendering after it. - Give up and alert someone after about 15 minutes.
- On an error with
retryable: true, back off with jitter and poll again. Stop on any other error.
A batch’s pictures are followed through imagePending and imagesPendingCount. They have no job id. get_generation_job follows a render you start on a single post, and is described under Editing posts.
Wait for the batch
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
async function waitForBatch(batchId: string) {
const startedAt = Date.now()
let delay = 5_000
for (;;) {
try {
const status = await callNative('get_post_batch_status', { batchId })
if (status.settled && (status.imagesPendingCount ?? 0) === 0) return status
} catch (err) {
// Poll again only when Native says the same call could succeed.
if (!(err instanceof NativeError) || !err.retryable) throw err
}
const elapsed = Date.now() - startedAt
if (elapsed > 15 * 60_000) throw new Error('Batch did not finish within 15 minutes')
// 5 seconds for the first minute, then 15, growing to no more than 30.
if (elapsed > 60_000) delay = Math.min(30_000, Math.max(15_000, delay * 1.5))
await sleep(delay + Math.random() * 1_000)
}
}
const done = await waitForBatch(batchId)
for (const item of done.items) {
if (item.status !== 'created') {
console.warn('Slot ' + item.index + ' ended as ' + item.status + ': ' + item.error?.code)
continue
}
// Show item.destinations to the reviewer and keep item.contentDigest
// for approve_post. Until those fields ship, call view_post with item.postId.
}Response, with the item fields in development
{
"batchId": "6b1f3c2a-8d4e-4f7a-9c1b-2e5d7a9f0c34",
"brandId": "8f0c2b7e-1d3a-4c5f-9e8b-7a6d5c4b3a21",
"companyId": "c2a7e9d1-5b3f-4e8a-a1c6-9d0b2f4e7a58",
"status": "partial",
"settled": true,
"requestedCount": 2,
"createdCount": 1,
"failedCount": 1,
"pendingCount": 0,
"imagesPendingCount": 0,
"warnings": [],
"error": null,
"items": [
{
"index": 0,
"status": "created",
"postId": "3d8e1a52-7c4b-4f19-b2a6-5e0d9c8f7a13",
"title": "Autumn opening hours",
"platforms": ["INSTAGRAM", "FACEBOOK"],
"preview": "Autumn hours start on Monday.",
"hasMedia": true,
"imagePending": false,
"mediaRequiredFor": [],
"error": null,
"appUrl": "https://app.native.no/api/open-post?id=3d8e1a52-7c4b-4f19-b2a6-5e0d9c8f7a13",
"destinations": [
{
"platform": "INSTAGRAM",
"text": "Autumn hours start on Monday. We open at 7 on weekdays, so the first cinnamon buns are out before your train.",
"firstComment": null,
"title": null,
"imageUrls": ["https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png"],
"images": [
{
"url": "https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png",
"slideText": "Autumn hours from Monday",
"slideTextSource": "layers",
"altText": "A tray of cinnamon buns on the counter by the window"
}
],
"thumbnailUrl": null,
"characterCount": 109,
"characterLimit": 2200,
"publishingOptions": null
},
{
"platform": "FACEBOOK",
"text": "Autumn hours start on Monday. We open at 7 on weekdays, so the first cinnamon buns are out before your train.",
"firstComment": null,
"title": null,
"imageUrls": ["https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png"],
"images": [
{
"url": "https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png",
"slideText": "Autumn hours from Monday",
"slideTextSource": "layers",
"altText": "A tray of cinnamon buns on the counter by the window"
}
],
"thumbnailUrl": null,
"characterCount": 109,
"characterLimit": 63206,
"publishingOptions": null
}
],
"imageUrls": ["https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png"],
"contentDigest": "v3.ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
"approval": null
},
{
"index": 1,
"status": "failed",
"postId": null,
"title": null,
"platforms": [],
"preview": null,
"hasMedia": false,
"imagePending": false,
"mediaRequiredFor": [],
"error": {
"code": "ai_generation_failed",
"message": "AI generation failed for this post.",
"retryable": true
},
"destinations": [],
"imageUrls": [],
"contentDigest": null,
"approval": null
}
],
"createdAt": "2026-10-12T08:00:03.000Z",
"updatedAt": "2026-10-12T08:02:41.000Z",
"appOrigin": "https://app.native.no"
}Read one post
#view_postPreviewReads one post exactly as it would publish: every destination with its own caption, first comment, title, pictures and options. This is what a reviewer should see before approving.
Input
idstringRequiredThe post id.
Output
idstringRequiredThe post id.
postIdstringRequiredThe same value as
id.authorNamestringRequiredThe name the post appears under.
authorSubtitlestring | nullRequiredA short line under the name, such as the post’s status and time.
authorAvatarUrlstring | nullRequiredThe picture beside the name.
postTextstringRequiredThe post’s main caption as plain text, or the first destination’s caption when the post has no main caption. Read
destinationsfor each destination’s own caption.imageUrlstring | nullRequiredThe first picture.
imageUrlsstring[]RequiredEvery slide, in order.
destinationsobject[]RequiredOne per destination. See the destination object.
contentDigeststring | nullRequiredThe version of the content an approval locks. See Content digest.
approvalobject | nullRequiredThe latest review decision, or
null. See the approval object.appOriginstringRequiredThe Native app’s address.
appUrlstringRequiredThe post in the Native app.
Errors: only the common ones, such as not_found when no post has that id, and forbidden for a post in a workspace you cannot reach.
The destination object
#Each entry is one place the post publishes. These are the fields the content digest covers, except the subtitle files sent with videos, which the API does not return.
Fields
platformstringRequiredThe destination, as an uppercase name such as
INSTAGRAM.textstringRequiredThis destination’s caption, as plain text.
firstCommentstring | nullRequiredThe first comment.
titlestring | nullRequiredThe publishable title, for a LinkedIn document, a TikTok slideshow or a YouTube video.
imageUrlsstring[]RequiredThis destination’s pictures, in order.
imagesobject[]OptionalNew, in development: one entry per
imageUrlsentry, in the same order. See Slide text and alt text.urlstringRequiredThe picture.
slideTextstring | nullRequiredThe text drawn on the slide, as data.
nullwhen Native has no record of it.slideTextSourcestring | nullRequiredWhere
slideTextcomes from, which says how exact it is.nullwithslideText.One of:
layers,promptaltTextstring | nullRequiredThe alt text written for the picture.
nullwhen none was written.
thumbnailUrlstring | nullRequiredThe video cover.
characterCountintegerRequiredThe length of
text.characterLimitintegerRequiredThis destination’s caption limit.
0means no limit is on record. A caption over its limit is refused when you schedule.publishingOptionsobject | nullRequiredOptions for this platform, such as a Reddit subreddit and flair or a TikTok post’s visibility. The keys differ per platform.
Response, with the image fields in development
{
"id": "3d8e1a52-7c4b-4f19-b2a6-5e0d9c8f7a13",
"postId": "3d8e1a52-7c4b-4f19-b2a6-5e0d9c8f7a13",
"authorName": "Solsiden Bakery",
"authorSubtitle": "Draft",
"authorAvatarUrl": "https://storage.googleapis.com/example-bucket/logos/solsiden.png",
"postText": "Autumn hours start on Monday. We open at 7 on weekdays, so the first cinnamon buns are out before your train.",
"imageUrl": "https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png",
"imageUrls": ["https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png"],
"destinations": [
{
"platform": "INSTAGRAM",
"text": "Autumn hours start on Monday. We open at 7 on weekdays, so the first cinnamon buns are out before your train.",
"firstComment": null,
"title": null,
"imageUrls": ["https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png"],
"images": [
{
"url": "https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png",
"slideText": "Autumn hours from Monday",
"slideTextSource": "layers",
"altText": "A tray of cinnamon buns on the counter by the window"
}
],
"thumbnailUrl": null,
"characterCount": 109,
"characterLimit": 2200,
"publishingOptions": null
},
{
"platform": "FACEBOOK",
"text": "Autumn hours start on Monday. We open at 7 on weekdays, so the first cinnamon buns are out before your train.",
"firstComment": null,
"title": null,
"imageUrls": ["https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png"],
"images": [
{
"url": "https://storage.googleapis.com/example-bucket/posts/3d8e/slide-1.png",
"slideText": "Autumn hours from Monday",
"slideTextSource": "layers",
"altText": "A tray of cinnamon buns on the counter by the window"
}
],
"thumbnailUrl": null,
"characterCount": 109,
"characterLimit": 63206,
"publishingOptions": null
}
],
"contentDigest": "v3.ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
"approval": null,
"appOrigin": "https://app.native.no",
"appUrl": "https://app.native.no/api/open-post?id=3d8e1a52-7c4b-4f19-b2a6-5e0d9c8f7a13"
}Content digest and approval
#view_post returns a contentDigest and an approval. A created batch item will carry the same two fields once the item fields in development ship. contentDigest is a stamp of everything that would publish, and approve_post records the one the reviewer saw. approval is the latest review decision, or null. Review and approval explains what the digest covers and how Native checks it before a post goes out.
Three things matter for posts that were just generated:
- A picture that lands changes the digest. Approve a post only once its batch item’s
imagePendingisfalse. - Approve the digest from the read the reviewer saw:
view_posttoday, or a created batch item once those fields ship. If the post changed in between,approve_postis refused withpost_content_changed. Read it again and have it reviewed again. - Never resend an approval with a newer digest the reviewer did not see.
Slide text and alt text
#slideText is the text drawn on a slide, as data. Use it to run your own checks on what a picture says, without OCR. slideTextSource says how exact it is.
slideTextSource | What slideText is |
|---|---|
layers | Read back off the design the picture was rendered from, so it is exactly the text on the picture. Words inside a placed photo are pixels, so they are not included. |
prompt | What the image model was asked to draw. It usually reproduces it exactly, but not always. |
null | Native has no record of the text, for example on an uploaded photo. slideText is null too. |
altText is the alt text written for a picture. Native sends it with the picture on platforms that take alt text. It is null when none was written, and also while Native’s backend is older than the change that adds it.
The digest covers alt text whenever any picture on a destination has some, so a change to it makes an approval stale. Show alt text to the reviewer along with the pictures.
A video has no images entry, so alt text on a video is digested but not returned. The Native app clears alt text on videos, so this should not come up in practice.
Pictures
#Picture URLs are public Google Cloud Storage addresses. They are unsigned and do not expire, so you can show them directly or copy them. Replacing or removing a picture on a post does not guarantee that its old URL stops working, so do not count on an edit to revoke access. A changed picture always has a new URL, so the post gets a new contentDigest. Limits has the details.
Feed and story posts
#- Every destination on a post shares the same picture in each slot.
- Story destinations (
INSTAGRAM_STORIES,FACEBOOK_STORIES) target 9:16. Feed destinations target 4:5. - A 9:16 picture fails the Instagram feed check when you schedule. Put feed and story content in separate batches, rather than mixing
INSTAGRAMandINSTAGRAM_STORIESon one post. - Each post also gets the brand’s default destination, so a batch you meant for stories can still carry a feed destination. Check each item’s
platforms, and remove a feed destination withupdate_postandremovePlatformswhen a post should be stories only. - Stories publish without a caption, and need a single picture or video.
- Not confirmed yet: whether the first render of a stories-only post from
create_postscomes out 9:16. Check the size of the pictureview_postreturns until this is confirmed. See aspect ratios.
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.