# Approval (/docs/api/concepts/approval)



Approval is the checkpoint between "content generated" and "content leaves the building." It's a flag on each [content item](/docs/api/concepts/content-items) and a policy on each [project](/docs/api/concepts/projects). The API enforces the gate; you build the review UX.

The current model is deliberately small: a boolean on the project ("should this project require approval") and a counter ("how many posts per user does that apply to"). A planned trust-score path will let approved accounts graduate off review automatically. Both live on the same fields.

## The gate [#the-gate]

Two things decide whether a content item can be scheduled:

1. **Project policy.** `requires_approval: boolean` and `first_n_posts_blocked: int`. Policy lives on the project row and applies to every content item the project generates.
2. **Content status.** Every content item has an `approval_status` of `not_required` | `pending` | `approved` | `rejected`. New containers on an approval-required project start at `pending`; the rest default to `not_required`.

The gate triggers on `POST /v1/content/:containerId/schedule` (and on `/publish`, which is just "schedule with `scheduledFor: now`"). If the project requires approval, the schedule call is refused with `403 APPROVAL_REQUIRED` until the container flips to `approved`.

```text
requires_approval = true  AND
approval_status != 'approved'
        →  403 APPROVAL_REQUIRED on schedule
```

<Callout type="warn">
  Approval is enforced at the schedule endpoint and at the just-before-publish check. Defense-in-depth on every platform publish path (Instagram Reels, TikTok drafts, Managed publish) is planned. If you're calling the publish pipeline directly via platform services, don't — go through the schedule endpoint so the gate runs.
</Callout>

## `firstNPostsBlocked` is informational [#firstnpostsblocked-is-informational]

Projects carry a `first_n_posts_blocked` field on their approval policy and a `current_blocked_count` that starts at `0`. These are intended for your UI — for example, rendering "2 of 3 reviewed" to an end user so they know how many more posts will hit the queue before the gate naturally relaxes.

Today the server-side gate fires whenever `requiresApproval: true` and the container isn't `approved` — there is no automatic decay from the counter. To turn the gate off for a project, `PATCH /v1/projects/:id/approval-policy` and set `requiresApproval: false`. An automatic decay / counter-driven disable is on the roadmap for a later phase.

## Approving and rejecting [#approving-and-rejecting]

```http
POST /v1/content/:containerId/approve
{
  "note": "On-brand, clean caption"
}

POST /v1/content/:containerId/reject
{
  "reason": "Wrong influencer for this product"
}
```

* `approve` flips `approvalStatus` to `approved`, stamps `approvedAt` and `approvedBy` (the API key id that approved), persists the `note`, and increments `currentBlockedCount` on the project. `note` is optional, max 1024 chars.
* `reject` flips `approvalStatus` to `rejected`, stamps `rejectedAt` and `rejectedBy` (API key id), persists `reason`. `reason` is required, 1–1024 chars. Rejected containers stay in the project for audit — they don't get deleted. To regenerate after a rejection, call [`POST /v1/content/:id/regenerate`](/docs/api/reference/content/regenerate-content) with a revised brief.

Actor attribution: both endpoints stamp `approvedBy` / `rejectedBy` with the calling API key's id. If you're building a review UI with multiple human reviewers, persist the reviewer's identity in your own audit table keyed by the container id. The partner-API audit log ties the action to the API key that signed the request.

Rejection is a terminal decision for that container. Calling `schedule` or `publish` against a `rejected` container returns `409 CONTENT_REJECTED` — the gate never opens for it, even if project policy later changes. To move forward, [regenerate](/docs/api/reference/content/regenerate-content) with a revised brief or create a fresh container.

Once scope enforcement ships, these endpoints will require `content:approve`. Today, org-scoped keys can call them directly.

## Reading the policy [#reading-the-policy]

```http
GET /v1/projects/:projectId/approval-policy
→ 200
{
  "requiresApproval": true,
  "firstNPostsBlocked": 3,
  "currentBlockedCount": 1
}
```

Patch the same shape with `PATCH` to change either field. Scope: `content:approve`.

## What's deferred [#whats-deferred]

A planned extension adds `trust_threshold` and `current_trust_score` to the policy. The threshold is a number; the score is computed from reviewer agreement and downstream engagement. When the score crosses the threshold, the project auto-transitions to continuous approval — no need to flip `requires_approval` manually.

If you want trust-based flow today, implement it in your own system and call `PATCH /approval-policy` to flip `requiresApproval` off when you're confident.
