# Generating posts Source: https://docs.native.no/native-api/generating-posts > **Preview.** The Native API is not live yet. This page describes the planned interface; names and fields can change before access opens. Write up to 20 draft posts from one brief, follow the batch until it settles, and read each post exactly as it would publish. > **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. 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 1. Start a batch with `create_posts` and store the `batchId`. 2. Poll `get_post_batch_status` until `settled` is `true` and `imagesPendingCount` is `0`. 3. Read each created post. Once the item fields in development ship, each created item carries its destinations, pictures, `contentDigest` and `approval`. Until then, call `view_post` for each `postId`. 4. Show the reviewer what you read. Then 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). 5. To change a post before review, see [Editing posts](https://docs.native.no/native-api/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 MCP tool `create_posts` (preview) Creates 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: - `brandId` (string, required): The client’s brand. - `brief` (string, required): What the posts are about, in plain words. 1 to 4,000 characters. - `count` (integer, required): How many posts to write, from 1 to 20. - `mediaIds` (string[], optional): Up to 10 brand library image ids. Every post in the batch gets the same pictures. Cannot be combined with `includeImage: true`. - `platforms` (string[], optional): Destinations 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’s `platforms` in `get_post_batch_status` shows where a post ended up. - `scheduledAt` (ISO 8601 string, optional): An intended publishing time, recorded on each post. It does not schedule anything. A time with a numeric offset is converted to UTC. - `includeImage` (boolean, optional): Render a picture for each post. On by default when you send no `mediaIds`. `true` together with `mediaIds` is refused, because the render would replace the pictures you sent. Output: - `batchId` (string, required): Pass it to `get_post_batch_status`. - `brandId` (string, required): The brand the batch is for. - `companyId` (string, required): The client’s workspace. - `status` (string, required): `queued` when the batch is created. - `requestedCount` (integer, required): The `count` you asked for. - `includeImage` (boolean, optional): Whether Native will render a picture for each post, once the default is applied. May be absent. - `items` (object[], required): One slot per requested post. - `index` (integer, required): The slot’s position, from 0. - `status` (string, required): `pending` at first. - `connectionWarning` (string, optional): Present 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](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`. Start a batch: ```ts // 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.batchId ``` Response: ```json { "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](https://docs.native.no/native-api/profiles#wait-for-the-build). | 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. ### 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 MCP tool `get_post_batch_status` (preview) Reads a batch and changes nothing, so you can call it as often as the polling guidance below allows. Input: - `batchId` (string, required): From `create_posts`. Output: - `batchId` (string, required): The batch. - `brandId` (string, required): The brand the batch is for. - `companyId` (string, required): The client’s workspace. - `status` (string, required; one of: `queued`, `generating`, `partial`, `ready`, `failed`): Where the batch is. Branch on `settled`, not on this. - `settled` (boolean, required): Copy generation has finished: `true` once `status` is `ready`, `partial` or `failed`. It does not mean every post was created, or that every picture has finished. - `requestedCount` (integer, required): The `count` you asked for. - `createdCount` (integer, required): Slots that became posts. - `failedCount` (integer, required): Slots that ended without a post. Counts both `failed` and `skipped` items. - `pendingCount` (integer, required): Slots still `pending` or `generating`. - `imagesPendingCount` (integer, optional): Created posts whose picture is still rendering. May be absent. - `warnings` (string[], optional): Notes 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. - `error` (string | null, optional): Why the batch failed, when it failed as a whole. May be absent. - `items` (object[], required): One per slot, in order. - `index` (integer, required): The slot’s position, from 0. - `status` (string, required; one of: `pending`, `generating`, `created`, `failed`, `skipped`): `skipped` means the batch stopped before this slot, for example because the workspace’s AI allowance ran out. `error` says why. - `postId` (string | null, required): Set once the slot is `created`. - `title` (string | null, required): The post’s title. - `platforms` (string[], required): The post’s destinations. - `preview` (string | null, optional): How the post opens, so you can check that the posts differ. May be absent. - `hasMedia` (boolean, optional): The post has at least one picture or video. May be absent. - `imagePending` (boolean, optional): The post exists and its copy is done, but its picture is still rendering. May be absent. - `mediaRequiredFor` (string[], optional): Destinations that cannot publish this post until it has media, such as a story. May be absent. - `error` (object | null, required): Why the slot did not become a post. `null` otherwise. - `code` (string, required; one of: `usage_limit_exceeded`, `ai_generation_failed`, `platform_not_connected`, `platform_not_supported`, `reddit_options_required`, `media_required`, `bad_request`, `internal_error`): Branch on this. Treat a code you do not know by its `retryable` flag. - `message` (string, required): Written by Native for people. It can change at any time. - `retryable` (boolean, required): Whether the same request, unchanged, could succeed. - `appUrl` (string, optional): The post in the Native app. Only on created slots. - `destinations` (object[], optional): New, in development: each destination exactly as `view_post` returns it. See [the destination object](https://docs.native.no/native-api/generating-posts#destination-object). Empty on a slot that holds no post. - `imageUrls` (string[], optional): New, in development: every slide, in order. Empty on a slot that holds no post. - `contentDigest` (string | null, optional): New, in development: the same value `view_post` returns. It changes when a pending picture lands, so approve only once `imagePending` is `false`. `null` on a slot that holds no post. - `approval` (object | null, optional): New, in development: the latest review decision, shaped like [the approval object](https://docs.native.no/native-api/approval#approval-object). `null` when there is none. - `createdAt` (ISO 8601 string, optional): When the batch was created. May be absent. - `updatedAt` (ISO 8601 string, optional): When the batch last changed. May be absent. - `appOrigin` (string, required): The Native app’s address, such as `https://app.native.no`. > **Until the item fields ship.** Today a created item has no `destinations`, `imageUrls`, `contentDigest` or `approval`. Call `view_post` for each `postId` to show the post and approve it. Once the fields ship, they are on every created slot. ### 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 `settled` is `true` and `imagesPendingCount` is `0`. `settled` covers 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](https://docs.native.no/native-api/editing-posts#get-generation-job). Wait for the batch: ```ts 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: ```json { "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 MCP tool `view_post` (preview) Reads 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: - `id` (string, required): The post id. Output: - `id` (string, required): The post id. - `postId` (string, required): The same value as `id`. - `authorName` (string, required): The name the post appears under. - `authorSubtitle` (string | null, required): A short line under the name, such as the post’s status and time. - `authorAvatarUrl` (string | null, required): The picture beside the name. - `postText` (string, required): The post’s main caption as plain text, or the first destination’s caption when the post has no main caption. Read `destinations` for each destination’s own caption. - `imageUrl` (string | null, required): The first picture. - `imageUrls` (string[], required): Every slide, in order. - `destinations` (object[], required): One per destination. See [the destination object](https://docs.native.no/native-api/generating-posts#destination-object). - `contentDigest` (string | null, required): The version of the content an approval locks. See [Content digest](https://docs.native.no/native-api/generating-posts#content-digest). - `approval` (object | null, required): The latest review decision, or `null`. See [the approval object](https://docs.native.no/native-api/approval#approval-object). - `appOrigin` (string, required): The Native app’s address. - `appUrl` (string, required): The 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: - `platform` (string, required): The destination, as an uppercase name such as `INSTAGRAM`. - `text` (string, required): This destination’s caption, as plain text. - `firstComment` (string | null, required): The first comment. - `title` (string | null, required): The publishable title, for a LinkedIn document, a TikTok slideshow or a YouTube video. - `imageUrls` (string[], required): This destination’s pictures, in order. - `images` (object[], optional): New, in development: one entry per `imageUrls` entry, in the same order. See [Slide text and alt text](https://docs.native.no/native-api/generating-posts#slide-text-and-alt-text). - `url` (string, required): The picture. - `slideText` (string | null, required): The text drawn on the slide, as data. `null` when Native has no record of it. - `slideTextSource` (string | null, required; one of: `layers`, `prompt`): Where `slideText` comes from, which says how exact it is. `null` with `slideText`. - `altText` (string | null, required): The alt text written for the picture. `null` when none was written. - `thumbnailUrl` (string | null, required): The video cover. - `characterCount` (integer, required): The length of `text`. - `characterLimit` (integer, required): This destination’s caption limit. `0` means no limit is on record. A caption over its limit is refused when you schedule. - `publishingOptions` (object | null, required): Options 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: ```json { "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](https://docs.native.no/native-api/approval#content-digest) 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 `imagePending` is `false`. - Approve the digest from the read the reviewer saw: `view_post` today, or a created batch item once those fields ship. If the post changed in between, `approve_post` is refused with `post_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 > **In development.** The `images` list on each destination is in development. Nothing in this section is returned today. `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](https://docs.native.no/native-api/limits#asset-urls) 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 `INSTAGRAM` and `INSTAGRAM_STORIES` on 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 with `update_post` and `removePlatforms` when 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_posts` comes out 9:16. Check the size of the picture `view_post` returns until this is confirmed. See [aspect ratios](https://docs.native.no/native-api/limits#aspect-ratios).