Native APITools
PreviewReview and approval
Record a client’s approval of the exact version they saw, or a rejection with a reason, and see what Native checks before a post goes out.
Your client reviews posts in your own product. Native records each decision and checks it again when the post is scheduled or published.
An approval names one exact version of a post. Every post carries a contentDigest, a stamp of everything that would be published. approve_post records the digest the reviewer saw. When the post is scheduled or published, Native works out the digest again. If the content has changed since the approval, the approval no longer counts.
How a review works
#- Read the post with
view_post. Keep itscontentDigest. - Show the reviewer every destination in
destinations[]: the caption, first comment, title, images, video cover and publishing options. The preview card alone is not the whole post. - Call
approve_postwithexpectedDigestset to thecontentDigestfrom that same read, and name the reviewer. Or callreject_postwith their reason. - Schedule or publish the post. Native checks the decision again at that moment. See scheduling and publishing.
Approve only once every image has landed. A picture that is still rendering changes the digest when it arrives, and the approval is then stale.
New, in development: created items in get_post_batch_status, the result of create_draft, and get_post_status will carry the same contentDigest and approval as view_post. You will then be able to show and approve a new post without reading it again. Until that ships, read the post with view_post first.
What the digest covers
#contentDigest is an opaque string. Today it looks like v3. followed by a SHA-256 hash, but the format can change. Compare it only for equality.
It covers which destinations the post has and, for each destination:
- the caption
- the first comment
- the publishable title
- the video cover (
thumbnailUrl) - the publishing options, such as a Reddit subreddit or TikTok visibility
- the image URLs, in order
- the alt text of its pictures, when any picture on that destination has some (new, in development)
- the post’s own title, on a LinkedIn or TikTok destination without a title of its own (new, in development)
- the subtitle files sent with its videos
It does not cover the scheduled time. Moving a post to another time keeps its approval.
Any edit changes the digest, whichever way it is made: through this API, in the Native app, or by Native’s own image workers. Adding or removing a destination changes it too. A changed image always has a new URL, so it always gives a new digest.
view_post.destinations[] shows everything the digest covers except the subtitle files, which this API does not return. On a video post, a subtitle change rolls the digest even though nothing you can show the reviewer has changed. If you review video posts, you cannot show their subtitles from this API yet.
New, in development: the same goes for the post’s own title. A LinkedIn or TikTok destination without a title of its own publishes the post’s title instead, and a LinkedIn one falls back to the brand’s name when the post has no title. destinations[] shows each destination’s own title and options, not the post’s title, so a change to the post’s title in the Native app can make an approval stale with nothing new to show.
Approve a post
#approve_postPreviewRecords approval of the post’s exact current content.
Input
idstringRequiredThe post id.
expectedDigeststringRequiredThe
contentDigestfrom the read the reviewer actually saw. 1 to 200 characters.reviewerNamestringOptionalThe person in your product who approved. 1 to 200 characters.
reviewerEmailstringOptionalThat person’s email address. A valid address of up to 320 characters.
If you send neither reviewer field, Native records the caller as the reviewer. It records the caller beside the reviewer either way.
Show the post, then approve it
const viewed = await client.callTool(
{ name: "view_post", arguments: { id: postId } },
undefined,
{ timeout: 150_000 },
);
const post = (viewed.structuredContent as { data: any }).data;
// Show post.destinations to the reviewer. When they approve:
const result = await client.callTool(
{
name: "approve_post",
arguments: {
id: postId,
expectedDigest: post.contentDigest,
reviewerName: "Kari Nordmann",
reviewerEmail: "kari@example.com",
},
},
undefined,
{ timeout: 150_000 },
);
const data = (result.structuredContent as { data: any }).data;
if (result.isError) {
if (data.error.code === "post_content_changed") {
// The post changed after the reviewer saw it.
// Read it again and show it to them again.
}
} else {
console.log(data.approval.valid); // true
}Never resend with a fresher digest that the reviewer did not see. If the post changed, show it again and let them decide again.
Output
idstringRequiredThe post id.
postIdstringOptionalThe same value as
id. New, in development: until the declared output schema ships, this field is absent.contentDigeststring | nullRequiredThe digest the decision was recorded against.
approvalobject | nullRequiredThe decision just recorded, as described in the approval object. After an approval,
validistrue.appOriginstringOptionalNew, in development: the origin of the Native app. Absent until then.
appUrlstringOptionalNew, in development: a link to the post in the Native app. Absent until then.
The rest of the post comes back too, without a declared schema: title, status, scheduledAt, companyId, brandId, owner, origin, groupKey, platformVariants[] with captions as plain text, createdAt and updatedAt. Ignore any field you do not use.
| Code | Retryable | Meaning |
|---|---|---|
post_content_changed | false | The post changed after the read the reviewer saw. Nothing was recorded. Read the post again and have it reviewed again. |
post_already_published, already_published | false | A destination of the post is live, and a post with a live destination can no longer be reviewed. publishedPlatforms lists the destinations that are live. Which of the two codes you get depends on the post’s state, so handle both. |
not_found | false | The id is wrong, or the post is not yours. |
bad_request | false | The input is invalid. invalidFields and fieldErrors say what to fix. |
Any tool can also return the common codes, such as internal_error or upstream_timeout. They are listed under errors.
Reject a post
#reject_postPreviewRecords a rejection with a reason. While the latest decision is a rejection, schedule_post and publish_post_now refuse the post with approval_rejected.
Editing the post does not clear a rejection. Only a new approve_post does, so the post needs a fresh review after it is fixed.
Input
idstringRequiredThe post id.
reasonstringRequiredWhy the reviewer turned the post down, in their words. 1 to 2,000 characters. It comes back as
approval.reason.expectedDigeststringOptionalThe
contentDigestthe reviewer saw, so the rejection names that version. Optional, but recommended. 1 to 200 characters.reviewerNamestringOptionalThe person in your product who rejected the post. 1 to 200 characters.
reviewerEmailstringOptionalThat person’s email address. A valid address of up to 320 characters.
Reviewer identity works as in approve_post. The output has the same shape as approve_post’s, with approval.decision set to REJECTED.
| Code | Retryable | Meaning |
|---|---|---|
post_content_changed | false | Only when you sent expectedDigest: the post changed after the reviewer saw it. Nothing was recorded. Read it again before deciding. |
already_scheduled | false | New, in development: the post is booked to publish, and a rejection would not stop it. Nothing was recorded. Withdraw each booked destination with unschedule_platform first, then reject. This error carries no lockedDestinations, so read each destination’s platformPostId from get_post_status. |
post_already_published, already_published | false | A destination of the post is live, and a post with a live destination can no longer be reviewed. publishedPlatforms lists the destinations that are live. |
not_found | false | The id is wrong, or the post is not yours. |
bad_request | false | The input is invalid, for example a reason that is only spaces. invalidFields and fieldErrors say what to fix. |
The approval object
#The latest decision on a post has the same shape everywhere it appears: in view_post, in the results of approve_post and reject_post, and (new, in development) in get_post_status, create_draft and created get_post_batch_status items. It is null until someone records a decision.
Fields
decisionstringRequiredThe latest decision on the post.
One of:
APPROVED,REJECTEDreviewerNamestring | nullRequiredWho decided, as you named them, or the caller when you named nobody.
reviewerEmailstring | nullRequiredTheir email address, when known.
reasonstring | nullRequiredThe reason given with a rejection.
nullon an approval.decidedAtISO 8601 stringRequiredWhen the decision was recorded.
approvedDigeststringRequiredThe digest the decision was recorded against. A rejection carries it too, despite the name.
validbooleanRequiredfalsewhen the content has changed since the decision.
approval
{
"decision": "APPROVED",
"reviewerName": "Kari Nordmann",
"reviewerEmail": "kari@example.com",
"reason": null,
"decidedAt": "2026-11-02T08:14:03.000Z",
"approvedDigest": "v3.9f2c41d07a6b3e58c1d4f0a92b7e6c35d8a1f4e07b29c6d3a5e8f1b0c47d2a96",
"valid": true
}What Native checks before a post goes out
#Each time a post is scheduled or published, Native works out its digest again and compares it with the latest decision.
| Situation | Result |
|---|---|
| The latest decision is an approval and the digest matches. | Allowed. |
| The content changed after the approval. | approval_stale. Read the post again, have it reviewed, and approve it again. |
| The latest decision is a rejection. | approval_rejected, even after edits. |
| No decision is on record, and the workspace requires approval. | New, in development: approval_required. Partner workspaces will require approval by default. Record a decision with approve_post. |
| No decision is on record, and approval is not required. | Allowed. This is how every workspace behaves today. |
| The workspace requires approval, and publishing would change the approved pictures, because the post’s design is stale or its media would need repair. | New, in development: design_changed_after_approval. Nothing is sent. Read the post again with view_post, have it reviewed, and approve it again. |
All of these codes have retryable: false: something has to change before the same call can succeed.
schedule_post and publish_post_now refuse the whole call. reschedule_posts checks each post on its own: a refusal shows up in that entry’s moves[].error, and the other entries still run. See scheduling and publishing.
Today, a post with no decision on record can be scheduled and published on every workspace. If your product promises that nothing goes out without the client’s approval, enforce that on your side until approval_required ships.
What may not change after approval
#On a workspace that requires approval, nothing Native does will change approved content:
- No design re-render. A post whose design is stale is refused, not re-rendered.
- No media repair, such as copying pictures from the main design onto a destination.
- No re-crop.
- No silent re-approval when someone presses Schedule in the Native app. The schedule fails with
approval_stale, and the app asks for an explicit approval. - A LinkedIn document PDF is built from the approved images.
- A branding change never re-renders a post that already exists, approved or not.
The one change Native still makes is format conversion, for example to JPEG for Instagram. Where any other change would be needed, the publish is refused with design_changed_after_approval and nothing is sent.
Recovering from a design change
#No tool renders a stale design on its own. An edit that re-renders the design, such as generate_image, produces new pictures. These pages do not cover generate_image yet: tools/list has its schema. Read the post again with view_post, have the new pictures reviewed, and approve the post again.
Good practice
#- Keep the digest you showed next to the decision in your own records. It tells you later exactly which version the client approved.
- Name the reviewer with
reviewerNameandreviewerEmail. Without them, the record names the caller, not the person who decided. - Read
approval.validbefore you schedule.falsemeans the content changed after the decision. - After any edit, show the post again and ask for a new approval.
update_post,generate_caption,regenerate_post_imageand changes to the destinations all change the digest. - Pass a rejection’s
approval.reasonto whoever fixes the post.
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.