# Review and approval Source: https://docs.native.no/native-api/approval > **Preview.** The Native API is not live yet. This page describes the planned interface; names and fields can change before access opens. 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. > **Partner access is not live yet.** The tools on this page are in Native’s gateway today, for people signed in to Native. Partner access with client-credentials tokens is in development, so a partner backend cannot call them yet. Anything marked **in development** is not deployed yet and can still change. [Authentication](https://docs.native.no/authentication) explains how partner access will work. 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 1. Read the post with `view_post`. Keep its `contentDigest`. 2. 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. 3. Call `approve_post` with `expectedDigest` set to the `contentDigest` from that same read, and name the reviewer. Or call `reject_post` with their reason. 4. Schedule or publish the post. Native checks the decision again at that moment. See [scheduling and publishing](https://docs.native.no/native-api/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 MCP tool `approve_post` (preview) Records approval of the post’s exact current content. Input: - `id` (string, required): The post id. - `expectedDigest` (string, required): The `contentDigest` from the read the reviewer actually saw. 1 to 200 characters. - `reviewerName` (string, optional): The person in your product who approved. 1 to 200 characters. - `reviewerEmail` (string, optional): That 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: ```ts 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: - `id` (string, required): The post id. - `postId` (string, optional): The same value as `id`. New, in development: until the declared output schema ships, this field is absent. - `contentDigest` (string | null, required): The digest the decision was recorded against. - `approval` (object | null, required): The decision just recorded, as described in [the approval object](https://docs.native.no/native-api/approval#approval-object). After an approval, `valid` is `true`. - `appOrigin` (string, optional): New, in development: the origin of the Native app. Absent until then. - `appUrl` (string, optional): New, 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](https://docs.native.no/native-api/errors). ## Reject a post MCP tool `reject_post` (preview) Records 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: - `id` (string, required): The post id. - `reason` (string, required): Why the reviewer turned the post down, in their words. 1 to 2,000 characters. It comes back as `approval.reason`. - `expectedDigest` (string, optional): The `contentDigest` the reviewer saw, so the rejection names that version. Optional, but recommended. 1 to 200 characters. - `reviewerName` (string, optional): The person in your product who rejected the post. 1 to 200 characters. - `reviewerEmail` (string, optional): That 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: - `decision` (string, required; one of: `APPROVED`, `REJECTED`): The latest decision on the post. - `reviewerName` (string | null, required): Who decided, as you named them, or the caller when you named nobody. - `reviewerEmail` (string | null, required): Their email address, when known. - `reason` (string | null, required): The reason given with a rejection. `null` on an approval. - `decidedAt` (ISO 8601 string, required): When the decision was recorded. - `approvedDigest` (string, required): The digest the decision was recorded against. A rejection carries it too, despite the name. - `valid` (boolean, required): `false` when the content has changed since the decision. approval: ```json { "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](https://docs.native.no/native-api/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 > **In development.** This section describes behaviour that is still in development. Until it ships, Native can still re-render a stale design or repair a post’s media after approval, so the pictures that publish can differ from the ones the reviewer approved. 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 `reviewerName` and `reviewerEmail`. Without them, the record names the caller, not the person who decided. - Read `approval.valid` before you schedule. `false` means 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_image` and changes to the destinations all change the digest. - Pass a rejection’s `approval.reason` to whoever fixes the post.