# 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. For publish-now (no future timestamp), use [`POST /v1/content/:containerId/publish`](/docs/api/reference/publishing/publish-content) — same target shape, no `scheduledFor`.

<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.

<Callout type="warn">
  **Atomic batch semantics.** Up to 50 targets per call. Validation runs across **every** target before any DB write — the first target that fails (bad `socialAccountId`, invalid `mode` enum, account not on this project, `tiktokMusic` set on a `draft` target, `shareReelToFeed` set on a non-Instagram / non-video / non-publish target, …) aborts the entire call. Zero `scheduled_posts` rows are written on failure; there is no partial-success array. Treat the call as all-or-nothing and split into smaller batches if you need partial writes.
</Callout>

<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. Recommended.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'scheduledFor', type: 'string (ISO 8601, UTC Z)', required: true, description: 'When to publish. **Must be in the future** — a ~30s clock-skew tolerance applies, but anything further in the past returns `422 VALIDATION` with `details.issues[0].path = &#x22;scheduledFor&#x22;`. To publish immediately, call [`POST /v1/content/:containerId/publish`](/docs/api/reference/publishing/publish-content) instead — `/schedule` with a past timestamp is a sign the wrong endpoint was picked.' },
  { name: 'targets', type: 'Target[]', required: true, description: 'One entry per destination account. At least one required, max 50. All-or-nothing on validation failure (see Errors).' },
]"
/>

<Callout type="warn">
  **Time zones.** `scheduledFor` is interpreted as a literal UTC instant, regardless of the project's `timezone` field. The project's `timezone` controls cron-driven managed-distribution slot generation; it does **NOT** shift partner-supplied `scheduledFor` values. To schedule "9am every weekday in the user's local timezone," convert in your application code first (e.g. `new Date("2026-05-21T09:00:00-04:00").toISOString()` → `"2026-05-21T13:00:00Z"`). Response timestamps (`scheduledFor`, `createdAt`, `updatedAt`, `publishedAt`) are likewise always UTC with the `Z` suffix.
</Callout>

<Parameters
  title="Target"
  rows="[
  { name: 'socialAccountId', type: 'string', required: true, description: 'Account id from list-social-accounts. Must already be connected to the same project as the container.' },
  { name: 'mode', type: 'string', required: true, enum: ['publish', 'draft', 'managed'], description: 'How the post is delivered. See &#x22;Modes&#x22; below.' },
  { name: 'captionOverride', type: 'string', description: 'Replace the container\'s caption for this target only. Up to 4000 chars. The publisher reads this in preference to `content_containers.caption`.' },
  { name: 'firstCommentOverride', type: 'string', description: 'Replace the container\'s first comment for this target only. Up to 4000 chars.' },
  { name: 'tiktokPostSettings', type: 'object', description: 'TikTok-only knobs. See &#x22;TikTok post settings&#x22; below. Ignored on `instagram` targets and on `managed` mode.' },
  { name: 'tiktokMusic', type: 'object', description: 'TikTok music selection for image / slideshow posts. See &#x22;TikTok music&#x22; below. Ignored on `instagram` targets. **Rejected with `422 VALIDATION`** when set on a `draft` target (TikTok inbox drafts have no music slot — the creator picks audio on-device). Omit on draft.' },
  { name: 'shareReelToFeed', type: 'boolean', description: 'Instagram Reels placement (mirrors Meta\'s Graph API `share_to_feed`). `true` (default) — the Reel appears in the Reels tab AND on the main profile grid. `false` — Reels tab only, skip the grid. **Rejected with `422 VALIDATION`** when set on (a) non-Instagram targets, (b) non-video content containers (slideshow / single-image), or (c) any mode other than `publish`. See &#x22;Instagram Reels placement&#x22; on [`/publish`](/docs/api/reference/publishing/publish-content#instagram-reels-placement) — same behavior here.' },
]"
/>

### Modes [#modes]

| Mode      | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `publish` | Layers posts to the connected platform automatically at `scheduledFor`.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `draft`   | Pushes the media as a draft to the user's mobile app — TikTok inbox or Instagram SMS. The end-user finishes posting from their phone. On success the polled status flips to `draft` (terminal from Layers' side — the creator finalizes on-device, invisible to us). On a platform handoff failure the row lands at `failed` with `lastError` populated and no retry; the wire enum is invariant — `draft` always means "delivered to device", `failed` always means "publisher tried and couldn't deliver." |
| `managed` | Dispatches via the project's connected managed-distribution provider. Requires the social account to belong to a layer with `template_id = af058068-ad85-4fb8-92a1-f531b40bfcbc` (managed distribution) and the layer's `config.provider` set.                                                                                                                                                                                                                                                               |

<Callout type="info">
  IG placement (Reels vs feed) is determined by the container's `media_type` (image → feed, video → reels, multi → carousel). There is no `feed`/`reels` mode on the API integration — the publisher picks the placement automatically.
</Callout>

<Callout type="warn">
  **Instagram requires a Business or Creator account.** Instagram Graph API publishing is not available on Personal accounts. A `publish` target pointed at a Personal-tier IG account lands at `failed` with `lastError` carrying the Graph API rejection message. Switch the account to Creator (free) in the Instagram mobile app's *Account type and tools* settings and re-issue.
</Callout>

<Callout type="info">
  **`externalUrl` for Instagram.** On `published` rows for `platform: "instagram"`, `externalUrl` carries the Graph API permalink (`/p/<shortcode>` or `/reel/<shortcode>`) — the publisher captures it from `?fields=permalink` after publish. In rare network-blip cases that fetch fails and `externalUrl` stays `null` even though `externalId` and `publishedAt` are populated and the post is live. TikTok URLs are derived deterministically from the handle + numeric id, so they don't have this edge case. Documented on the read side at [`GET /v1/scheduled-posts/:id`](/docs/api/reference/publishing/get-post#status-values).
</Callout>

<Callout type="info">
  **TikTok-only fields on Instagram targets.** `tiktokPostSettings` and `tiktokMusic` are silently ignored on `platform: "instagram"` targets — they don't map to any Instagram Graph API knob. If you're building a generic schedule helper that always sets these fields, expect them to be no-ops for IG; they're not a 422.
</Callout>

<Callout type="info">
  `draft` for Instagram requires the **organization's primary operator** to have a verified phone number; the SMS draft is sent to that operator. Without a verified phone the publisher's auto-publish loop falls back and the wire `status` lands at `failed`.
</Callout>

### TikTok post settings [#tiktok-post-settings]

`tiktokPostSettings` mirrors the UI's "Advanced settings" panel for TikTok. All fields are optional; missing fields fall back to TikTok's `creator_info` defaults (typically the most permissive privacy + interactions enabled).

| Field              | Type                                                                                      | Notes                                                                                                                                                                                                 |
| ------------------ | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `privacyLevel`     | `"PUBLIC_TO_EVERYONE" \| "MUTUAL_FOLLOW_FRIENDS" \| "FOLLOWER_OF_CREATOR" \| "SELF_ONLY"` | Required by TikTok for direct posts. The publisher defaults to `PUBLIC_TO_EVERYONE` when omitted.                                                                                                     |
| `disableComment`   | boolean                                                                                   | Disable comments on the published post.                                                                                                                                                               |
| `disableDuet`      | boolean                                                                                   | Disable Duet (videos only).                                                                                                                                                                           |
| `disableStitch`    | boolean                                                                                   | Disable Stitch (videos only).                                                                                                                                                                         |
| `isBrandOrganic`   | boolean                                                                                   | "Your Brand" toggle — labels the post as Promotional content.                                                                                                                                         |
| `isBrandedContent` | boolean                                                                                   | "Branded Content" toggle — labels the post as Paid Partnership. &#x2A;*Required by TikTok ToS / FTC §255 when a creator is paid by a brand to post.** Partners running paid promotions must set this. |

### TikTok music [#tiktok-music]

`tiktokMusic` mirrors the UI's "Background Music" panel for TikTok image / slideshow posts. Applies to `publish` and `managed` only — TikTok inbox drafts (`draft`) cannot stamp music since the creator picks it on-device, and the schema rejects the field on `draft` targets with `422 VALIDATION` (rather than silently dropping it).

| Field     | Type                           | Notes                                                                                                                                                         |
| --------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`    | `"none" \| "auto" \| "manual"` | Required. `none` opts out of music. `auto` lets TikTok auto-suggest a trending sound. `manual` uses `trackId`.                                                |
| `trackId` | string (UUID)                  | Required when `mode: "manual"`. Track id from [`GET /v1/tiktok-music`](/docs/api/reference/publishing/list-tiktok-music). 422 if the id isn't in the catalog. |

```json
"tiktokMusic": { "mode": "auto" }
```

```json
"tiktokMusic": { "mode": "manual", "trackId": "8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999" }
```

<Callout type="info">
  **`manual` on `publish` currently degrades to `auto`.** TikTok's photo-publish API requires a Commercial Music Library id, which the trending-music catalog does not surface — `manual` and `auto` produce the same `auto_add_music: true` payload on direct publish. This matches the Layers UI's behavior. **Managed-distribution targets DO honor the manual track**: the publisher receives a resolved `tiktokMusicLink` and uses it verbatim. Use `managed` mode if you need exact track selection on TikTok image posts.
</Callout>

<Callout type="info">
  **Two device-handoff paths.** `mode: "draft"` here pushes a draft to the platform-native inbox (TikTok inbox / Instagram SMS draft) at `scheduledFor`. For the UI's "Text me this post" / Elle iMessage flow — sending media + caption + posting instructions to an arbitrary phone number now, not at a future slot — call [`POST /v1/content/:containerId/notify-device`](/docs/api/reference/publishing/notify-device).
</Callout>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/content/cnt_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999/schedule \
      -H "Authorization: Bearer lp_..." \
      -H "Idempotency-Key: 8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999" \
      -H "Content-Type: application/json" \
      -d '{
        "scheduledFor": "2026-04-19T14:00:00Z",
        "targets": [
          { "socialAccountId": "sa_71b2a4e5-8c3f-4d1a-9e7b-2c5d8f0a1b22", "mode": "publish" },
          {
            "socialAccountId": "sa_5d2e9f08-1c4a-4b6e-9f3d-7a2b0c4d6e88",
            "mode": "draft",
            "captionOverride": "Fresh pour, ready now."
          },
          {
            "socialAccountId": "sa_a9c3b7f1-2e6d-4a08-b51c-9f3e1d7b2c44",
            "mode": "publish",
            "tiktokPostSettings": {
              "privacyLevel": "PUBLIC_TO_EVERYONE",
              "isBrandedContent": true,
              "disableDuet": false,
              "disableStitch": false
            }
          }
        ]
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const result = await layers.publishing.schedule(
      {
        containerId: "cnt_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
        scheduledFor: "2026-04-19T14:00:00Z",
        targets: [
          { socialAccountId: "sa_71b2a4e5-8c3f-4d1a-9e7b-2c5d8f0a1b22", mode: "publish" },
          {
            socialAccountId: "sa_5d2e9f08-1c4a-4b6e-9f3d-7a2b0c4d6e88",
            mode: "draft",
            captionOverride: "Fresh pour, ready now.",
          },
        ],
      },
      { 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_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
        scheduled_for="2026-04-19T14:00:00Z",
        targets=[
            {"socialAccountId": "sa_71b2a4e5-8c3f-4d1a-9e7b-2c5d8f0a1b22", "mode": "publish"},
            {"socialAccountId": "sa_5d2e9f08-1c4a-4b6e-9f3d-7a2b0c4d6e88", "mode": "draft",
             "captionOverride": "Fresh pour, ready now."},
        ],
        idempotency_key=str(uuid.uuid4()),
    )
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="Scheduled">
  ```json
  {
    "scheduledPostIds": [
      "sp_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
      "sp_4c8e7d2f-9a1b-4c3d-8e7f-2a1b3c4d5e60"
    ],
    "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_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
      "sp_4c8e7d2f-9a1b-4c3d-8e7f-2a1b3c4d5e60"
    ],
    "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&#x60;, **`scheduledFor` in the past** (`details.issues[0].path = "scheduledFor"`; a \~30s clock-skew tolerance applies), bad `mode` enum, or schema-level cross-field violations (e.g. `tiktokMusic` set on a `draft` target, or `shareReelToFeed` set on a non-Instagram / non-video / non-publish target — see "TikTok music" and the [`/publish`'s Instagram Reels placement](/docs/api/reference/publishing/publish-content#instagram-reels-placement) section). When raised by `shareReelToFeed`, `details.reason` is one of `"non_instagram_target"` (also includes `details.platform` and `details.socialAccountId`) or `"non_video_container"` (also includes `details.mediaType`). All issues are reported in a single `issues[]` array. |
| 422    | `SOCIAL_ACCOUNT_NOT_PUBLISH_READY` | A target's social account is connected to the project but isn't yet provisioned to publish in the requested mode. `details.socialAccountId` and `details.mode` identify the offending target. Verify the social account's setup for this project before retrying.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 401    | `UNAUTHENTICATED`                  | Missing or invalid key.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| 403    | `FORBIDDEN_SCOPE`                  | Key lacks `publish:write`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 404    | `NOT_FOUND`                        | Container or **any single** target's social account is not in your organization. &#x2A;*The whole batch fails on the first missing account — no partial writes.**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 409    | `CONFLICT`                         | Container not `completed`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 409    | `CONFLICT_DUPLICATE_ID`            | A partner-supplied `scheduledPostId` already exists. Pick a fresh `sp_<uuid>` and retry — same id can't be reused across calls (DB unique constraint, not a replay key; pair with `Idempotency-Key` if you need request-level replay).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 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-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
