# POST /v1/content/:containerId/approve (/docs/api/reference/approval/approve-content)



<Endpoint method="POST" path="/v1/content/:containerId/approve" scope="content:approve" phase="1" />

Flips `approvalStatus` from `pending` to `approved`, stamps the reviewer, and — if the project is still counting down first-N-blocked posts — decrements the block counter. From the moment this call returns, the container is schedulable.

This call is what unblocks [`schedule-content`](/docs/api/reference/publishing/schedule-content) when the project's approval policy requires review. Without approval, scheduling returns `APPROVAL_REQUIRED`.

## Path parameters [#path-parameters]

<Parameters
  rows="[
  { name: 'containerId', type: 'string (uuid)', required: true, description: 'The container to approve.' },
]"
/>

## Body [#body]

<Parameters
  title="Body (all optional)"
  rows="[
  { name: 'note', type: 'string', description: 'Free-text note, max 1024 chars. Stored on the container for audit.' },
]"
/>

## Request [#request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```sh title="terminal"
    curl -X POST https://api.layers.com/v1/content/{containerId}/approve \
      -H "X-Api-Key: $LAYERS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "note": "Looks good. Shipping tomorrow 7am." }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts title="approve.ts"
    const res = await fetch(
      `https://api.layers.com/v1/content/${containerId}/approve`,
      {
        method: 'POST',
        headers: {
          'X-Api-Key': process.env.LAYERS_API_KEY!,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ note: 'Approved by agent.' }),
      },
    );
    const result = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```py title="approve.py"
    import os, httpx

    r = httpx.post(
        f"https://api.layers.com/v1/content/{container_id}/approve",
        headers={
            "X-Api-Key": os.environ["LAYERS_API_KEY"],
            "Content-Type": "application/json",
        },
        json={"note": "Approved."},
    )
    result = r.json()
    ```
  </Tab>
</Tabs>

## Responses [#responses]

<Response status="200" description="Approved. The container is ready to schedule.">
  ```json
  {
    "id": "cnt_01HXZ9...",
    "approvalStatus": "approved",
    "approvedAt": "2026-04-18T09:40:12Z",
    "approvedBy": "api_key_c2037bb9..."
  }
  ```
</Response>

<Response status="200" description="Approved AND a previously-blocked schedule was promoted to live scheduled_posts.">
  ```json
  {
    "id": "cnt_01HXZ9...",
    "approvalStatus": "approved",
    "approvedAt": "2026-04-18T09:40:12Z",
    "approvedBy": "api_key_c2037bb9...",
    "pendingSchedulePromotion": {
      "status": "ok",
      "scheduledPostIds": [
        "sp_01HXZN4K5M6P7QRS8TUV9WXYZA"
      ]
    }
  }
  ```

  If a `pendingSchedulePromotion` block is present with `status: "failed"`, approval still flipped — but the queued schedule did not promote (e.g. the original target social account was removed). Re-issue [`schedule`](/docs/api/reference/publishing/schedule-content) to re-create the rows.
</Response>

<Response status="409" description="Container is already approved, rejected, or not in a reviewable state.">
  ```json
  {
    "error": {
      "code": "CONFLICT",
      "message": "Container is already approved.",
      "requestId": "req_...",
      "details": { "approvalStatus": "approved" }
    }
  }
  ```
</Response>

## Notes [#notes]

<Callout type="info">
  Approval is idempotent at the state level: if the container is already
  `approved`, we return `409 CONFLICT` rather than a silent no-op. That way
  your logic knows whether *this* call flipped the bit or a prior one did.
</Callout>

* **Who can approve.** Any API key with the `content:approve` scope.
* **First-N gate.** Each approval while `currentBlockedCount < firstNPostsBlocked` increments the counter. Once it hits the threshold, the gate stays open for the project's lifetime.
* **Publishing consequence.** Approving doesn't publish. It just removes the gate. [Schedule](/docs/api/reference/publishing/schedule-content) or [publish now](/docs/api/reference/publishing/publish-content) to actually put it on a surface.
* **Pending-schedule promotion.** If [`schedule`](/docs/api/reference/publishing/schedule-content) was called before approval, those targets were stashed on the container as `pendingSchedule` metadata and returned with `gateStatus: "blocked_on_approval"`. This call promotes them to live `scheduled_posts` rows atomically — the `scheduledPostIds` you already received from `schedule` become the ids of the new rows. The response surfaces the outcome under `pendingSchedulePromotion`.

## Errors [#errors]

| Code                | When                                                        |
| ------------------- | ----------------------------------------------------------- |
| `CONFLICT`          | Container is already approved, rejected, or not reviewable. |
| `VALIDATION_FAILED` | Container isn't `completed` yet.                            |
| `NOT_FOUND`         | Container id not in this org.                               |
| `FORBIDDEN_SCOPE`   | Key lacks `content:approve`.                                |

## See also [#see-also]

* [Reject a container](/docs/api/reference/approval/reject-content)
* [Read the approval policy](/docs/api/reference/approval/get-approval-policy)
* [Schedule approved content](/docs/api/reference/publishing/schedule-content)
