Native API· Generating posts

Native APITools

Preview

Generating 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

#
  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.
  5. 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

#
TOOLcreate_postsPreview

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

  • brandIdstringRequired

    The client’s brand.

  • briefstringRequired

    What the posts are about, in plain words. 1 to 4,000 characters.

  • countintegerRequired

    How many posts to write, from 1 to 20.

  • mediaIdsstring[]Optional

    Up to 10 brand library image ids. Every post in the batch gets the same pictures. Cannot be combined with includeImage: true.

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

  • scheduledAtISO 8601 stringOptional

    An intended publishing time, recorded on each post. It does not schedule anything. A time with a numeric offset is converted to UTC.

  • includeImagebooleanOptional

    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

  • batchIdstringRequired

    Pass it to get_post_batch_status.

  • brandIdstringRequired

    The brand the batch is for.

  • companyIdstringRequired

    The client’s workspace.

  • statusstringRequired

    queued when the batch is created.

  • requestedCountintegerRequired

    The count you asked for.

  • includeImagebooleanOptional

    Whether Native will render a picture for each post, once the default is applied. May be absent.

  • itemsobject[]Required

    One slot per requested post.

    • indexintegerRequired

      The slot’s position, from 0.

    • statusstringRequired

      pending at first.

  • connectionWarningstringOptional

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

Response

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

CoderetryableWhat to do
bad_requestfalseFix what invalidFields and fieldErrors name, then send the call again.
platform_not_connectedfalseA 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_supportedfalseNative does not publish to a destination you named. unsupportedPlatforms lists it and supportedPlatforms lists what you can use.
reddit_options_requiredfalseA 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_requiredfalseEvery 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_exceededfalseThe workspace’s AI allowance is used up. resetDate says when it refills.
ai_generation_failedtrueGeneration failed before the batch started. Send the call again.
profile_not_readytrueNew, 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

#
TOOLget_post_batch_statusPreview

Reads a batch and changes nothing, so you can call it as often as the polling guidance below allows.

Input

  • batchIdstringRequired

    From create_posts.

Output

  • batchIdstringRequired

    The batch.

  • brandIdstringRequired

    The brand the batch is for.

  • companyIdstringRequired

    The client’s workspace.

  • statusstringRequired

    Where the batch is. Branch on settled, not on this.

    One of: queued, generating, partial, ready, failed

  • settledbooleanRequired

    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.

  • requestedCountintegerRequired

    The count you asked for.

  • createdCountintegerRequired

    Slots that became posts.

  • failedCountintegerRequired

    Slots that ended without a post. Counts both failed and skipped items.

  • pendingCountintegerRequired

    Slots still pending or generating.

  • imagesPendingCountintegerOptional

    Created posts whose picture is still rendering. May be absent.

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

  • errorstring | nullOptional

    Why the batch failed, when it failed as a whole. May be absent.

  • itemsobject[]Required

    One per slot, in order.

    • indexintegerRequired

      The slot’s position, from 0.

    • statusstringRequired

      skipped means the batch stopped before this slot, for example because the workspace’s AI allowance ran out. error says why.

      One of: pending, generating, created, failed, skipped

    • postIdstring | nullRequired

      Set once the slot is created.

    • titlestring | nullRequired

      The post’s title.

    • platformsstring[]Required

      The post’s destinations.

    • previewstring | nullOptional

      How the post opens, so you can check that the posts differ. May be absent.

    • hasMediabooleanOptional

      The post has at least one picture or video. May be absent.

    • imagePendingbooleanOptional

      The post exists and its copy is done, but its picture is still rendering. May be absent.

    • mediaRequiredForstring[]Optional

      Destinations that cannot publish this post until it has media, such as a story. May be absent.

    • errorobject | nullRequired

      Why the slot did not become a post. null otherwise.

      • codestringRequired

        Branch on this. Treat a code you do not know by its retryable flag.

        One of: usage_limit_exceeded, ai_generation_failed, platform_not_connected, platform_not_supported, reddit_options_required, media_required, bad_request, internal_error

      • messagestringRequired

        Written by Native for people. It can change at any time.

      • retryablebooleanRequired

        Whether the same request, unchanged, could succeed.

    • appUrlstringOptional

      The post in the Native app. Only on created slots.

    • destinationsobject[]Optional

      New, in development: each destination exactly as view_post returns it. See the destination object. Empty on a slot that holds no post.

    • imageUrlsstring[]Optional

      New, in development: every slide, in order. Empty on a slot that holds no post.

    • contentDigeststring | nullOptional

      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.

    • approvalobject | nullOptional

      New, in development: the latest review decision, shaped like the approval object. null when there is none.

  • createdAtISO 8601 stringOptional

    When the batch was created. May be absent.

  • updatedAtISO 8601 stringOptional

    When the batch last changed. May be absent.

  • appOriginstringRequired

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

CodeWhat it means
usage_limit_exceededThe workspace’s AI allowance ran out during the batch.
ai_generation_failedWriting this post failed.
platform_not_connectedA destination for this post has no live connection.
platform_not_supportedNative does not publish to a destination for this post.
reddit_options_requiredA REDDIT destination had no Reddit options.
media_requiredEvery destination is a story, and the post has no media.
bad_requestNative refused the post as invalid.
internal_errorSomething 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.

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

#
TOOLview_postPreview

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

  • idstringRequired

    The post id.

Output

  • idstringRequired

    The post id.

  • postIdstringRequired

    The same value as id.

  • authorNamestringRequired

    The name the post appears under.

  • authorSubtitlestring | nullRequired

    A short line under the name, such as the post’s status and time.

  • authorAvatarUrlstring | nullRequired

    The picture beside the name.

  • postTextstringRequired

    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.

  • imageUrlstring | nullRequired

    The first picture.

  • imageUrlsstring[]Required

    Every slide, in order.

  • destinationsobject[]Required

    One per destination. See the destination object.

  • contentDigeststring | nullRequired

    The version of the content an approval locks. See Content digest.

  • approvalobject | nullRequired

    The latest review decision, or null. See the approval object.

  • appOriginstringRequired

    The Native app’s address.

  • appUrlstringRequired

    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

  • platformstringRequired

    The destination, as an uppercase name such as INSTAGRAM.

  • textstringRequired

    This destination’s caption, as plain text.

  • firstCommentstring | nullRequired

    The first comment.

  • titlestring | nullRequired

    The publishable title, for a LinkedIn document, a TikTok slideshow or a YouTube video.

  • imageUrlsstring[]Required

    This destination’s pictures, in order.

  • imagesobject[]Optional

    New, in development: one entry per imageUrls entry, in the same order. See Slide text and alt text.

    • urlstringRequired

      The picture.

    • slideTextstring | nullRequired

      The text drawn on the slide, as data. null when Native has no record of it.

    • slideTextSourcestring | nullRequired

      Where slideText comes from, which says how exact it is. null with slideText.

      One of: layers, prompt

    • altTextstring | nullRequired

      The alt text written for the picture. null when none was written.

  • thumbnailUrlstring | nullRequired

    The video cover.

  • characterCountintegerRequired

    The length of text.

  • characterLimitintegerRequired

    This destination’s caption limit. 0 means no limit is on record. A caption over its limit is refused when you schedule.

  • publishingOptionsobject | nullRequired

    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

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

#

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.

slideTextSourceWhat slideText is
layersRead 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.
promptWhat the image model was asked to draw. It usually reproduces it exactly, but not always.
nullNative 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 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.

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