# Content review (/docs/api/concepts/content-review)



Content review 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 policy — three modes [#the-policy--three-modes]

Every project has a content-review policy with one of three values:

| `policy`         | What it does                                                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `auto_approve`   | Generated content is publish-ready immediately. No human in the loop.                                                           |
| `review_first_n` | First N items require human approval. Once N have cleared review, every subsequent item auto-approves. Uses `firstN` to pick N. |
| `review_all`     | Every item requires human approval, forever.                                                                                    |

Pick `auto_approve` for full autonomy, `review_first_n` to validate the first batch from a new project / influencer / brand brief and then let the optimizer fly, and `review_all` for regulated verticals where every post must be human-checked before it ships.

`firstN` is **required** when policy is `review_first_n` and **forbidden** otherwise — the API returns `422 VALIDATION` on either violation.

## The gate [#the-gate]

Two things decide whether a content item can be scheduled:

1. **Project policy.** Set on the project's content-review policy resource (above).
2. **Content status.** Every content item has an `approvalStatus` of `not_required` | `pending` | `approved` | `rejected`. New containers on a `review_*` 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 container is `pending`, the schedule call is refused with `403 APPROVAL_REQUIRED` until the container flips to `approved`. If the container is `rejected`, the call is refused with `409 CONTENT_REJECTED` permanently.

```text
approvalStatus === 'pending'   →  403 APPROVAL_REQUIRED on schedule
approvalStatus === 'rejected'  →  409 CONTENT_REJECTED on schedule
```

<Callout type="warn">
  Approval is enforced at the schedule endpoint and at the just-before-publish check. Use the schedule endpoint for any publish flow that should respect the gate.
</Callout>

## `pendingCount` is the queue depth [#pendingcount-is-the-queue-depth]

The policy resource also reports a server-computed `pendingCount` — the number of containers in this project that are currently sitting in `approvalStatus: "pending"`. Read-only, useful for rendering "N items awaiting review" in your UI.

For `review_first_n`, the gate self-disables once the project has seen `firstN` containers move out of `pending` (any terminal state — approved, rejected — counts). Subsequent generations land at `not_required` automatically. This is checked at content-creation time, not at schedule time.

## 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 optional `note` (max 1024 chars).
* `reject` flips `approvalStatus` to `rejected`, stamps `rejectedAt` and `rejectedBy` (API key id), persists `reason` (required, 1–1024 chars). Rejected containers stay in the project for audit — they don't get deleted. To produce a new take, call [`POST /v1/projects/:id/content`](/docs/api/reference/content/slideshow-builder) again with a fresh `hook`.

Actor attribution: both endpoints return `approvedBy` / `rejectedBy` from the calling API key. If you're building a review UI with multiple human reviewers, persist the reviewer's identity in your own system.

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.

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

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

```http
GET /v1/projects/:projectId/content-review-policy
→ 200
{
  "projectId": "5e9c0b1e-...",
  "policy": "review_first_n",
  "firstN": 3,
  "pendingCount": 1,
  "updatedAt": "2026-04-25T22:55:10.996Z"
}
```

Patch with `PATCH` to change the policy. Send the new `policy` (and `firstN` if applicable):

```http
PATCH /v1/projects/:projectId/content-review-policy
{
  "policy": "review_first_n",
  "firstN": 5
}
```

To switch off the gate entirely:

```http
PATCH /v1/projects/:projectId/content-review-policy
{
  "policy": "auto_approve"
}
```

Sending `firstN` together with `auto_approve` or `review_all` returns `422 VALIDATION` with a clear `details.issues` pointer at `firstN`.
