POST /v1/content/:containerId/schedule
Schedule a content container to one or more connected accounts at a specific time. The approval gate can block unapproved content.
/v1/content/:containerId/schedule- Auth
- Bearer
- Scope
- publish:write
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 — same target shape, no scheduledFor.
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 promotes it into scheduled_posts automatically. If approvalStatus is rejected, the call returns 409 CONTENT_REJECTED. See Approval.
The container must be status: "completed" - you can't schedule something that's still generating.
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.
containerIdstringrequiredCompleted content container id.
Idempotency-Keystring (UUID)optionalSame key + same body replays the cached response. Recommended.
scheduledForstring (ISO 8601, UTC Z)requiredWhen 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 = "scheduledFor"`. 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.targetsTarget[]requiredOne entry per destination account. At least one required, max 50. All-or-nothing on validation failure (see Errors).
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.
socialAccountIdstringrequiredAccount id from list-social-accounts. Must already be connected to the same project as the container.modestringrequiredHow the post is delivered. See "Modes" below.One of:publish,draft,managedcaptionOverridestringoptionalReplace the container's caption for this target only. Up to 4000 chars. The publisher reads this in preference to `content_containers.caption`.firstCommentOverridestringoptionalReplace the container's first comment for this target only. Up to 4000 chars.tiktokPostSettingsobjectoptionalTikTok-only knobs. See "TikTok post settings" below. Ignored on `instagram` targets and on `managed` mode.tiktokMusicobjectoptionalTikTok music selection for image / slideshow posts. See "TikTok music" 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.shareReelToFeedbooleanoptionalInstagram 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 "Instagram Reels placement" on [`/publish`](/docs/api/reference/publishing/publish-content#instagram-reels-placement) — same behavior here.
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. |
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 partner API — the publisher picks the placement automatically.
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.
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.
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.
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.
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. 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
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. 422 if the id isn't in the catalog. |
"tiktokMusic": { "mode": "auto" }"tiktokMusic": { "mode": "manual", "trackId": "8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999" }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.
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.
Example request
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
}
}
]
}'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.
}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()),
)Response
{
"scheduledPostIds": [
"sp_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
"sp_4c8e7d2f-9a1b-4c3d-8e7f-2a1b3c4d5e60"
],
"gateStatus": "queued",
"scheduledFor": "2026-04-19T14:00:00Z"
}{
"scheduledPostIds": [
"sp_8f1d6c3e-4b2a-4a18-9e4f-c2d7a1b0e999",
"sp_4c8e7d2f-9a1b-4c3d-8e7f-2a1b3c4d5e60"
],
"gateStatus": "blocked_on_approval",
"scheduledFor": "2026-04-19T14:00:00Z"
}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 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
| Status | Code | When |
|---|---|---|
| 422 | VALIDATION | Empty targets, malformed scheduledFor, 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 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. 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
POST /v1/content/:id/publish- publish nowGET /v1/scheduled-posts/:id- poll statusPOST /v1/content/:id/approve- release the gate- Approval - how the gate works