Skip to main content
Publishing is two calls: build the post, then send it. They are separate on purpose — the split is where a human fits, if you want one.
1

Create the post

The post is created at pending_review. Nothing is public yet — it shows up in the studio’s review queue like any other post.channels defaults to that creator’s connected channels, so most calls need only account_id, asset_ids and caption. The format (photo, carousel or video) is derived from the media and validated against every target platform before the post is written — a carousel that a platform cannot take is a 400, not a failed publish an hour later. Pass format to declare it yourself instead.hashtags are bare words, without the #. They are appended to the caption when the post is sent, and they count against each platform’s caption limit.
2

Send it

3

Watch it land

Post settings

Everything the studio’s composer can set is available at creation. All of it is optional; each setting is applied only on the platform it belongs to.

Per-platform overrides

platform_settings takes caption and scheduled_for on every channel, plus title and visibility (public / unlisted / private) on YouTube. A caption set here replaces the shared one on that channel only; a scheduled_for here replaces the shared time. Two rules keep mistakes loud rather than silent: every key must name a channel the post actually targets, and an unrecognised setting is a 400 rather than a value quietly dropped on the way to the platform.

Saving as a draft

"draft": true hands the post to TikTok’s Creator Inbox instead of publishing it. The creator finishes and posts it from the TikTok app. The post reaches draft_sent, never published — there is no publish time to report, because the creator may never post it. This is orthogonal to timing: a draft can still be sent now, scheduled or queued. The mode decides when you hand it over, draft decides what you hand over.
Only TikTok can take a draft. A post that also targets IG, YouTube or Telegram is rejected with a 400 rather than quietly publishing those channels live.The draft path needs the video.upload OAuth scope where a direct post needs video.publish. A connection granted only the latter fails at publish time.

Posting from the creator’s phone

"physical_device": true sends the post from a real phone bound to that creator, a device that runs the account the way a person would, instead of going through a platform’s publishing API. Everything else about the call is unchanged: the post is still created at pending_review, and POST /v1/clips/{id}/publish is still what hands it over. Two things have to be true, and both are resolved server-side rather than taken from the request:
  1. Your workspace has phone posting enabled. It is off by default and turned on per workspace. Ask support to enable it.
  2. The creator is bound to a device. Binding happens in the studio, per creator.
Missing either is a 400 on physical_device saying which one, never a silently dropped destination. The phone is a destination like any channel, so it can run alongside them:
Or be the whole destination. A device-only post is physical_device plus an explicit empty channel list:
The empty array has to be sent: omitting channels still means “every connected channel”, so leaving it out would post to those as well. The post reads back "physical_device": true, and once the phone has accepted the hand-off it also carries device_post_id.
physical_device and draft cannot be combined. The connected phone posts directly and has no draft inbox to hand anything to, so the pair is a 400.

The review gate

POST /v1/clips/{id}/publish is the only call in this API that reaches a real audience, and it is not undoable. Treat it the way you would treat a deploy to production.
The states a post moves through:
Three rules hold regardless of who is calling:
  • A post enters at pending_review. The API cannot create one in any other state.
  • scheduled is reachable only from pending_review. Asking to publish an already published post is a 400 that says so.
  • published is written only after the platform confirms. If the hand-off fails, the post stays scheduled with the reason recorded, and you can retry — it is never falsely marked live.
Want a human in the loop? Create posts and never call publish. They queue up in the studio’s review screen, where an operator approves them exactly as they would a post the platform generated. The API gives you the operator’s authority; it does not force you to use it.
Every publish writes an audit entry naming the API key that triggered it, so “who posted this?” always has an answer.

Requirements

Channels are connected in the studio, per creator. A creator with none gets a 400 telling you so — the API cannot connect accounts on your behalf.The one exception is a device-only post: "physical_device": true with an explicit "channels": [] needs no connected channel, because the phone is the destination.
Use asset ids from a succeeded generation, or from a completed upload. An asset that is still uploading is rejected.
One video, or up to ten stills for a carousel. Platform-specific carousel limits are checked at creation.

From the CLI

crl posts create alone leaves the post in review. --publish now|schedule|queue chains the second call. Publishing from a terminal asks for confirmation; pass --yes in scripts. The settings above have flags too:
--format, --timezone and --draft map to the fields of the same name. The rest of platform_settings (per-platform captions and times) goes through --platform-settings '{"TikTok":{"caption":"…"}}'.