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



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

Publish now. If you need a pre-publish cancel window, use [`schedule`](/docs/api/reference/publishing/schedule-content) instead and cancel the scheduled post before it starts publishing.

<Callout type="warn">
  Nothing publishes until the container's approval flag is set. If the project has `requires_approval: true` and the container is not yet `approved`, this call returns `403 APPROVAL_REQUIRED` - flipping approval later will still publish, but at that later time, not immediately. Unlike [`schedule`](/docs/api/reference/publishing/schedule-content), publish does **not** stash a pending intent — partners should wait for approval and re-issue. See [Approval](/docs/api/concepts/approval).
</Callout>

<Callout type="warn">
  **Atomic batch semantics.** Up to 50 targets per call. Identical all-or-nothing behavior to [`schedule`](/docs/api/reference/publishing/schedule-content) — the first invalid `socialAccountId` aborts the whole call with no rows written.
</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. Especially important here - accidental retries double-post.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'targets', type: 'Target[]', required: true, description: 'Same shape as schedule. At least one required, max 50. All-or-nothing.' },
]"
/>

`Target` shape is identical to [`/schedule`](/docs/api/reference/publishing/schedule-content#target). All three modes (`publish`, `draft`, `managed`) are supported on this endpoint with the same per-target fields.

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

### Modes [#modes]

| Mode      | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `publish` | Layers posts to the connected platform automatically. Row lands at `published` (with `externalId` + `externalUrl` populated) on success, or `failed` (with `lastError` populated, no retry) on a hard publish failure.                                                                                                                                                                                                                                                                                       |
| `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.** Instagram permalinks use an opaque shortcode (`/p/<shortcode>` or `/reel/<shortcode>`) that can't be derived from the numeric `externalId` alone. The publisher captures the permalink from the Graph API's `?fields=permalink` after publish and surfaces it on `externalUrl`. A network blip on that follow-up fetch leaves `externalUrl: null` even though `externalId` and `publishedAt` are populated and the post is live — open the IG mobile app for the connected account to view it manually.
</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 publish 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>

### Instagram Reels placement [#instagram-reels-placement]

Videos posted to Instagram via `publish` go to **Reels** by default. Reels are short-form vertical videos and appear in two places on the connected account:

| Where it shows                               | When                                                            |
| -------------------------------------------- | --------------------------------------------------------------- |
| Reels tab on the profile                     | Always — every Reel lands here                                  |
| Main profile grid (with a play-icon overlay) | When `shareReelToFeed` is omitted or `true` (Graph API default) |
| Only the Reels tab, **not** the main grid    | When `shareReelToFeed: false`                                   |

`shareReelToFeed` maps verbatim to Meta's [`share_to_feed`](https://developers.facebook.com/docs/instagram-platform/content-publishing#reels-posts) parameter on the Reels media container.

| Media type                             | Placement                                                  | `shareReelToFeed` allowed? |
| -------------------------------------- | ---------------------------------------------------------- | -------------------------- |
| Single video                           | Reel + grid (default) or Reels tab only (`false`)          | ✅ Yes                      |
| Slideshow / carousel (multiple images) | **Grid only** — there is no Reels analog for carousels     | ❌ No — 422 VALIDATION      |
| Single image                           | **Grid only** — there is no Reels analog for single images | ❌ No — 422 VALIDATION      |

<Callout type="warn">
  **Slideshow and single-image Instagram posts are grid-only.** Setting `shareReelToFeed` on a non-video container returns `422 VALIDATION` (`details.reason: "non_video_container"`). The Graph API has no `share_to_feed` analog for `CAROUSEL` / `IMAGE` containers — placement is implicit.
</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) immediately. For the UI's "Text me this post" / Elle iMessage flow — sending media + caption + posting instructions to an arbitrary phone number, not to the connected creator account — 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/publish \
      -H "Authorization: Bearer lp_..." \
      -H "Idempotency-Key: 3a7b2f11-9e4c-4a12-9d8a-b5c7e2f0a111" \
      -H "Content-Type: application/json" \
      -d '{
        "targets": [
          { "socialAccountId": "sa_71b2a4e5-8c3f-4d1a-9e7b-2c5d8f0a1b22", "mode": "publish" }
        ]
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const result = await layers.publishing.publishNow(
      {
        containerId: "cnt_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
        targets: [
          { socialAccountId: "sa_71b2a4e5-8c3f-4d1a-9e7b-2c5d8f0a1b22", mode: "publish" },
        ],
      },
      { idempotencyKey: crypto.randomUUID() },
    );
    ```
  </Tab>

  <Tab value="Python">
    ```python
    result = layers.publishing.publish_now(
        container_id="cnt_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
        targets=[
            {"socialAccountId": "sa_71b2a4e5-8c3f-4d1a-9e7b-2c5d8f0a1b22", "mode": "publish"},
        ],
        idempotency_key=str(uuid.uuid4()),
    )
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="Queued for immediate publish">
  ```json
  {
    "scheduledPostIds": ["sp_4c8e7d2f-9a1b-4c3d-8e7f-2a1b3c4d5e60"]
  }
  ```
</Response>

The response shape differs from [`/schedule`](/docs/api/reference/publishing/schedule-content) — `/publish` omits both `gateStatus` and `scheduledFor`. Pending containers 403 outright (so the only successful state is "queued for immediate publish" and `gateStatus` would always be `"queued"`), and `scheduledFor` would always echo `now()` — partners that need timestamps poll [`GET /v1/scheduled-posts/:id`](/docs/api/reference/publishing/get-post) and read `attemptedAt` / `publishedAt`.

## Errors [#errors]

| Status | Code                               | When                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 422    | `VALIDATION`                       | Empty `targets`, invalid `mode`, or schema-level cross-field violations (e.g. `tiktokMusic` set on a `draft` target, or `shareReelToFeed` set on a non-Instagram target / non-video container / non-`publish` mode — see callouts above). 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`). |
| 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.                                                                                                                                                                                                |
| 403    | `APPROVAL_REQUIRED`                | Container not approved and project still in the approval window. &#x2A;*Publish does not stash intent — re-issue once approved.**                                                                                                                                                                                                                                                                                                                                |
| 404    | `NOT_FOUND`                        | Container or **any** target's account not in your organization (all-or-nothing).                                                                                                                                                                                                                                                                                                                                                                                 |
| 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.                                                                                                                                                                                                                                                                                                                                                                                |

## See also [#see-also]

* [`POST /v1/content/:id/schedule`](/docs/api/reference/publishing/schedule-content) - queue for later
* [`GET /v1/scheduled-posts/:id`](/docs/api/reference/publishing/get-post) - watch it publish
* [`GET /v1/projects/:id/scheduled-posts`](/docs/api/reference/publishing/list-posts) - list posts in a project
* [`DELETE /v1/scheduled-posts/:id`](/docs/api/reference/publishing/cancel-post) - cancel before it publishes
