# POST /v1/content/:containerId/schedule (/docs/api/reference/publishing/schedule-content)



<Endpoint method="POST" path="/v1/content/:containerId/schedule" auth="Bearer" scope="publish:write" phase="1" />

Schedule a completed content container to publish to N social accounts at a specific time. Each target gets its own `scheduledPostId` you can later reschedule or cancel.

<Callout type="warn">
  Nothing publishes until the container's approval flag is set. If the project has `requires_approval: true` and the container is still `approvalStatus: "pending"`, this call returns `202` with `gateStatus: "blocked_on_approval"` and stashes the intent on the container — a subsequent [`approve`](/docs/api/reference/approval/approve-content) promotes it into `scheduled_posts` automatically. If `approvalStatus` is `rejected`, the call returns `409 CONTENT_REJECTED`. See [Approval](/docs/api/concepts/approval).
</Callout>

The container must be `status: "completed"` — you can't schedule something that's still generating.

<Parameters
  title="Path"
  rows="[
  { name: 'containerId', type: 'string', required: true, description: 'Completed content container id.' },
]"
/>

<Parameters
  title="Headers"
  rows="[
  { name: 'Idempotency-Key', type: 'string (UUID)', description: 'Same key + same body replays the cached response for 24 hours. Recommended.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'scheduledFor', type: 'string (ISO-8601)', required: true, description: 'When to publish. Schedule at least 30 seconds in the future; past/near-now values are accepted but the dispatcher may publish immediately on the next tick.' },
  { name: 'targets', type: 'Target[]', required: true, description: 'One entry per destination account. At least one required.' },
]"
/>

<Parameters
  title="Target"
  rows="[
  { name: 'socialAccountId', type: 'string', required: true, description: 'Account id from list-social-accounts.' },
  { name: 'mode', type: 'string', required: true, enum: ['direct_publish', 'draft_to_device', 'reels', 'feed'], description: 'Publish mode. Not every mode is valid on every platform — see table below.' },
  { name: 'scheduledPostId', type: 'string', description: 'Partner-created id for this post. Server generates one if omitted.' },
  { name: 'captionOverride', type: 'string', description: 'Replace the container\'s caption for this target only.' },
  { name: 'firstCommentOverride', type: 'string', description: 'Replace the container\'s first comment for this target only.' },
]"
/>

### Valid mode per platform [#valid-mode-per-platform]

| Platform  | Modes                               |
| --------- | ----------------------------------- |
| TikTok    | `direct_publish`, `draft_to_device` |
| Instagram | `reels`, `feed`, `draft_to_device`  |

`draft_to_device` sends the media to the logged-in mobile app as a draft — the end-customer finishes posting by hand. Useful when platform ToS prefers a human tap.

<Callout type="info">
  The schedule request currently accepts any mode string per the enum; platform compatibility is enforced at publish time, not at schedule time. Sending a platform-incompatible mode will queue the post but fail during publish with a platform error. Strict schedule-time validation is planned.
</Callout>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/content/cnt_01HXZM3K4N5P6QRS7TUV8WXYZ9/schedule \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..." \
      -H "Idempotency-Key: 8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999" \
      -H "Content-Type: application/json" \
      -d '{
        "scheduledFor": "2026-04-19T14:00:00Z",
        "targets": [
          { "socialAccountId": "sa_01HXZ9P2M4N5KLM6TUV7WXYZ9A", "mode": "direct_publish" },
          { "socialAccountId": "sa_01HXZQ8N3C5R6STUV7WXYZ9AB", "mode": "reels", "captionOverride": "Fresh pour, ready in 30s." }
        ]
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const result = await layers.publishing.schedule(
      {
        containerId: "cnt_01HXZM3K4N5P6QRS7TUV8WXYZ9",
        scheduledFor: "2026-04-19T14:00:00Z",
        targets: [
          { socialAccountId: "sa_01HXZ9P2M4N5KLM6TUV7WXYZ9A", mode: "direct_publish" },
          {
            socialAccountId: "sa_01HXZQ8N3C5R6STUV7WXYZ9AB",
            mode: "reels",
            captionOverride: "Fresh pour, ready in 30s.",
          },
        ],
      },
      { idempotencyKey: crypto.randomUUID() },
    );

    if (result.gateStatus === "blocked_on_approval") {
      // Approve the container or leave the posts queued; they flip to queued once approved.
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    result = layers.publishing.schedule(
        container_id="cnt_01HXZM3K4N5P6QRS7TUV8WXYZ9",
        scheduled_for="2026-04-19T14:00:00Z",
        targets=[
            {"socialAccountId": "sa_01HXZ9P2M4N5KLM6TUV7WXYZ9A", "mode": "direct_publish"},
            {"socialAccountId": "sa_01HXZQ8N3C5R6STUV7WXYZ9AB", "mode": "reels",
             "captionOverride": "Fresh pour, ready in 30s."},
        ],
        idempotency_key=str(uuid.uuid4()),
    )
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="Scheduled">
  ```json
  {
    "scheduledPostIds": [
      "sp_01HXZN4K5M6P7QRS8TUV9WXYZA",
      "sp_01HXZN4K5M6P7QRS8TUV9WXYZB"
    ],
    "gateStatus": "queued",
    "scheduledFor": "2026-04-19T14:00:00Z"
  }
  ```
</Response>

<Response status="202" description="Gated on approval — intent stashed on the container, will flip to queued on approval">
  ```json
  {
    "scheduledPostIds": [
      "sp_01HXZN4K5M6P7QRS8TUV9WXYZA",
      "sp_01HXZN4K5M6P7QRS8TUV9WXYZB"
    ],
    "gateStatus": "blocked_on_approval",
    "scheduledFor": "2026-04-19T14:00:00Z"
  }
  ```
</Response>

`gateStatus: "queued"` (200) means `scheduled_posts` rows exist now and will attempt to publish at `scheduledFor`. `gateStatus: "blocked_on_approval"` (202) means the request was accepted but the rows do **not** yet exist — the intent is stashed on the container's `pendingSchedule` metadata; a subsequent [`approve`](/docs/api/reference/approval/approve-content) call promotes those intents into live `scheduled_posts` rows. The `scheduledPostIds` are the stable ids those rows will adopt on promotion, so it's safe to record them on your side immediately.

## Errors [#errors]

| Status | Code               | When                                                                                                |
| ------ | ------------------ | --------------------------------------------------------------------------------------------------- |
| 422    | `VALIDATION`       | Empty `targets`, malformed `scheduledFor`, or bad enum value.                                       |
| 401    | `UNAUTHENTICATED`  | Missing or invalid key.                                                                             |
| 403    | `FORBIDDEN_SCOPE`  | Key lacks `publish:write`.                                                                          |
| 404    | `NOT_FOUND`        | Container or a target account not in your organization.                                             |
| 409    | `CONFLICT`         | Container not `completed`, or a `scheduledPostId` you created already exists with a different body. |
| 409    | `CONTENT_REJECTED` | Container's `approval_status` is `rejected`. Regenerate or replace the container.                   |
| 429    | `RATE_LIMITED`     | Write budget exhausted.                                                                             |

## See also [#see-also]

* [`POST /v1/content/:id/publish`](/docs/api/reference/publishing/publish-content) — publish now
* [`GET /v1/scheduled-posts/:id`](/docs/api/reference/publishing/get-scheduled-post) — poll status
* [`POST /v1/content/:id/approve`](/docs/api/reference/approval/approve-content) — release the gate
* [Approval](/docs/api/concepts/approval) — how the gate works
