TikTok's Content Posting API lets an app publish videos and photo posts to a creator's account, or drop them in the creator's TikTok inbox to finish in the app. The calls are short. What takes the time is everything around them: an audit before anything can be public, a creator info call before every post, a posting screen TikTok specifies down to the checkboxes, and a daily cap shared with every other app the creator uses.
This guide covers those rules, with each one linked to TikTok's own docs, and the calls in curl.
The short version
- Two ways to post: Direct Post (
video.publish) publishes to the profile; Upload (video.upload) sends a draft to the creator's inbox (get started). - Until your app passes TikTok's audit, every post is private (
SELF_ONLY), the accounts must be private, and at most 5 people can post through the app in 24 hours (content sharing guidelines). - Before each post, call creator info and use only the privacy levels it returns (query creator info).
- TikTok fetches a file from your URL only if you verified the domain or URL prefix; a file on the person's device goes up in chunks instead (media transfer guide).
- A post is done when the status call says
PUBLISH_COMPLETE, not when the first call returns (get post status). - Around 15 direct posts a day per creator, shared across every app that posts for them (content sharing guidelines).
Direct Post or Upload
| Direct Post | Upload (inbox draft) | |
|---|---|---|
| Scope | video.publish |
video.upload |
| Video endpoint | /v2/post/publish/video/init/ |
/v2/post/publish/inbox/video/init/ |
| Caption, privacy, interactions | Your app sends them | The creator sets them in TikTok |
| What the creator does | Nothing after your app's screen | Opens the inbox notification and posts from TikTok's editor |
| Limit | About 15 posts a day per creator | At most 5 pending drafts in 24 hours |
Sources: direct post, upload, content sharing guidelines.
Upload is the easier start: the creator finishes the post in TikTok, so your app sends only the file. TikTok asks you to tell people they must tap the inbox notification to finish the post (upload). Direct Post is what you want for scheduling or for an agent that publishes, and it is the one the rules below are mostly about.
Photo posts use one endpoint for both, /v2/post/publish/content/init/, with post_mode set to DIRECT_POST or MEDIA_UPLOAD (photo post).
The audit: what an unaudited app can do
Getting the video.publish scope approved does not make posts public. Until the app passes a separate audit, TikTok's guidelines set these limits (content sharing guidelines):
- Every post is
SELF_ONLY: only the creator sees it. - Every account posting through the app must be set to private at the time of posting. A public account gets
unaudited_client_can_only_post_to_private_accounts(HTTP 403, direct post). - At most 5 people can post through the app in a 24-hour window.
- To make a post public later, the creator switches the account to public, then each post's privacy to "Everyone".
You apply for the audit from the Content Posting API page in the developer portal. The audit form also asks for your usage estimates, which set your app's daily cap on active creators (content sharing guidelines). The guidelines rule out apps built only for testing, so build the posting screen described below before you apply: it is what the reviewers look at.
Scopes and tokens
The person logs in with TikTok's Login Kit, and your server exchanges the code at https://open.tiktokapis.com/v2/oauth/token/ (get a user access token).
- The access token lasts 2 hours and the refresh token 180 days (manage access tokens). TikTok says to schedule refreshes from
expires_inandrefresh_expires_inrather than hard-code these numbers. - Each refresh returns a new refresh token and invalidates the one you sent. Store the new one every time: if a replaced token is used again, the person has to authorize your app again (same page).
- Two refreshes of the same login at the same time can lose it, since the second sends a token the first just replaced. Let one process own each login's refresh.
Step 1: Query creator info
TikTok requires this call each time you show the posting screen: it says who is posting, which privacy levels the account allows, and what the creator turned off (query creator info).
export TT_TOKEN="<USER_ACCESS_TOKEN>"
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/creator_info/query/" \
-H "Authorization: Bearer $TT_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8"{
"data": {
"creator_avatar_url": "https://...",
"creator_username": "autumnroasters",
"creator_nickname": "Autumn Roasters",
"privacy_level_options": ["FOLLOWER_OF_CREATOR", "MUTUAL_FOLLOW_FRIENDS", "SELF_ONLY"],
"comment_disabled": false,
"duet_disabled": true,
"stitch_disabled": true,
"max_video_post_duration_sec": 180
},
"error": { "code": "ok", "message": "", "log_id": "..." }
}What to do with each field (same page and the guidelines):
creator_nickname: show it on the posting screen, so the person sees which account will post.privacy_level_options: the only values you may send. A public account getsPUBLIC_TO_EVERYONE,MUTUAL_FOLLOW_FRIENDSandSELF_ONLY; a private one getsFOLLOWER_OF_CREATORinstead ofPUBLIC_TO_EVERYONE. Anything else fails withprivacy_level_option_mismatch.comment_disabled,duet_disabled,stitch_disabled: when true, grey out that option. Duet and Stitch are always off on private accounts.max_video_post_duration_sec: check the video's length against it before you send anything.- An error such as
spam_risk_too_many_postsorreached_active_user_caphere means stop: the guidelines say to end the attempt when the creator cannot post right now. Creator info returns these with HTTP 200, so checkerror.code, not the HTTP status.
Step 2: Start the post
With a URL, TikTok downloads the file itself. This needs the URL's domain or prefix verified in your app's settings in the developer portal; otherwise the call fails with url_ownership_unverified (media transfer guide). The URL must be https, must not redirect, and must stay reachable until the download ends, which times out after an hour.
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/video/init/" \
-H "Authorization: Bearer $TT_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d '{
"post_info": {
"title": "First roast of the season #coffee",
"privacy_level": "SELF_ONLY",
"disable_comment": false,
"disable_duet": true,
"disable_stitch": true,
"video_cover_timestamp_ms": 1000,
"brand_content_toggle": false,
"brand_organic_toggle": false,
"is_aigc": false
},
"source_info": {
"source": "PULL_FROM_URL",
"video_url": "https://media.example.com/videos/first-roast.mp4"
}
}'
# {"data":{"publish_id":"v_pub_url~v2.123456789"},"error":{"code":"ok",...}}The post_info fields (direct post):
privacy_level: required, one of the creator info options.SELF_ONLYis the only one that works before the audit.title: the caption, up to 2,200 UTF-16 code units, with hashtags and mentions.disable_comment,disable_duet,disable_stitch: what the person chose on your screen.video_cover_timestamp_ms: the cover frame; the first frame if unset.brand_content_toggle: a paid partnership for a third party.brand_organic_toggle: promoting the creator's own business. Send both, from the person's answer.is_aigc: adds TikTok's AI-generated label.
Keep the publish_id: it is how you follow the post.
Upload a file instead
If the file is on the person's device, use FILE_UPLOAD; TikTok's guidelines say files already on your servers must use PULL_FROM_URL. The first call says how the file is split; TikTok answers with an upload_url, valid for an hour, and you send the bytes to it with PUT (media transfer guide, direct post).
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/video/init/" \
-H "Authorization: Bearer $TT_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d '{
"post_info": { "title": "First roast of the season #coffee", "privacy_level": "SELF_ONLY",
"disable_comment": false, "disable_duet": true, "disable_stitch": true, "brand_content_toggle": false },
"source_info": { "source": "FILE_UPLOAD", "video_size": 12582912,
"chunk_size": 12582912, "total_chunk_count": 1 }
}'
# {"data":{"publish_id":"v_pub_file~v2-1.123456789","upload_url":"https://open-upload.tiktokapis.com/video/?upload_id=...&upload_token=..."},...}curl -s -X PUT "<UPLOAD_URL_WITH_ITS_QUERY_STRING>" \
-H "Content-Range: bytes 0-12582911/12582912" \
-H "Content-Length: 12582912" \
-H "Content-Type: video/mp4" \
--data-binary "@first-roast.mp4"The chunk rules trip people up, so here they are in full (same page):
- A file under 5 MB goes in one piece, with
chunk_sizeequal to the file size. - Otherwise each chunk is 5 to 64 MB, sent in order, at most 1,000 of them. A file over 64 MB must be split.
total_chunk_countisvideo_sizedivided bychunk_size, rounded down. The leftover bytes ride on the last chunk, which can be up to 128 MB.- Each
PUTanswers 206 while chunks are missing and 201 when the file is complete. A 403 means theupload_urlexpired: start again.
Step 3: Check the status
The init call only starts the work. Ask for the status with the publish_id until it settles, or subscribe to the post.publish.* webhooks (get post status).
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/status/fetch/" \
-H "Authorization: Bearer $TT_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d '{ "publish_id": "v_pub_url~v2.123456789" }'status |
Meaning |
|---|---|
PROCESSING_DOWNLOAD |
TikTok is pulling your URL |
PROCESSING_UPLOAD |
Your chunks are arriving |
SEND_TO_USER_INBOX |
The draft is in the creator's inbox (Upload only) |
PUBLISH_COMPLETE |
Posted |
FAILED |
Read fail_reason |
publicaly_available_post_id(TikTok's spelling) stays empty until the post is public and has passed moderation. Moderation usually takes under a minute but can take a few hours. A private post never gets one, since the ID is only returned for public posts (same page).- The status endpoint allows 30 requests a minute per token. Poll every few seconds, back off, and stop at a time limit you choose.
- Tell the person it may take a few minutes for the post to appear: the guidelines ask for that notice.
Photo posts
Photos go to /v2/post/publish/content/init/ and must come from URLs: there is no file upload for photos (photo post). Up to 35 images (same page), JPEG or WebP, at most 20 MB and 1080p each (media transfer guide).
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/content/init/" \
-H "Authorization: Bearer $TT_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d '{
"media_type": "PHOTO",
"post_mode": "DIRECT_POST",
"post_info": {
"title": "Autumn menu",
"description": "Three new drinks this week #coffee",
"privacy_level": "SELF_ONLY",
"disable_comment": false,
"auto_add_music": true,
"brand_content_toggle": false,
"brand_organic_toggle": false
},
"source_info": {
"source": "PULL_FROM_URL",
"photo_cover_index": 0,
"photo_images": [
"https://media.example.com/photos/menu-1.webp",
"https://media.example.com/photos/menu-2.webp",
"https://media.example.com/photos/menu-3.webp"
]
}
}'
# {"data":{"publish_id":"p_pub_url~v2.123456789"},...}Photos differ from video in a few fields: title takes 90 UTF-16 code units and description 4,000; only comments can be turned off (no Duet or Stitch); auto_add_music adds music on a direct post; photo_cover_index picks the cover, counting from 0 (same page).
The posting screen TikTok requires
This is where most audits are won or lost. Before your app sends anything, the person must see a screen with (content sharing guidelines):
- The creator's nickname, from creator info.
- A preview of the content.
- A title they can edit, including any text or hashtags you filled in.
- Privacy as a dropdown of the creator info options, with no default.
- Allow Comment, Duet and Stitch (photos: Comment only), all unchecked to start, greyed out where creator info says disabled.
- A commercial content disclosure toggle, off by default, with "Your brand" and "Branded content". If it is on with neither picked, publishing is disabled. Branded content cannot be private.
- The declaration before the publish button: "By posting, you agree to TikTok's Music Usage Confirmation", plus the Branded Content Policy when branded content is picked.
And no watermark, logo or promotional text added to the person's content. Nothing is sent to TikTok until the person expressly agrees (same page).
For an agent this means one thing: it cannot decide for the person. An agent can draft the caption and pick the media, but privacy, the interaction settings and the disclosure have no defaults, so it has to ask, show the creator's nickname, and get a clear yes before it calls the init endpoint. A tool that takes these as required arguments and refuses a missing one keeps the agent honest.
Limits
| Limit | Value | Source |
|---|---|---|
| Direct posts per creator | About 15 a day, shared with every app using Direct Post | Guidelines |
| Pending inbox drafts | 5 in 24 hours | Upload |
| Creators per app | Set from your audit form; 5 in 24 hours before the audit | Guidelines |
| Init calls (video, upload, photo) | 6 a minute per token | Direct post |
| Creator info | 20 a minute per token | Query creator info |
| Status | 30 a minute per token | Get post status |
TikTok does not publish the exact daily cap: the guidelines say "typically around 15". Since other apps the creator uses count toward it, your own count can be under 15 and still hit spam_risk_too_many_posts. Treat that error as "try later or post from the app", never as a reason to retry.
Media requirements
From the media transfer guide:
| Video | Photo | |
|---|---|---|
| Formats | MP4 (recommended), MOV, WebM | JPEG, WebP |
| Codecs | H.264 (recommended), H.265, VP8, VP9 | |
| Size | Up to 4 GB | Up to 20 MB each |
| Dimensions | 360 to 4096 px on each side | Up to 1080p |
| Frame rate | 23 to 60 fps | |
| Length | Direct Post: up to the creator's max_video_post_duration_sec; Upload: up to 10 minutes |
Up to 35 images (photo post) |
All creators can post 3-minute videos and some can post 5 or 10, which is why the duration comes from creator info rather than a constant (same page).
Common errors
| Error | What it means | What to do |
|---|---|---|
unaudited_client_can_only_post_to_private_accounts |
Unaudited app, public account | Set the account to private, or pass the audit |
privacy_level_option_mismatch |
A privacy value creator info did not offer | Use only privacy_level_options |
url_ownership_unverified |
The video or photo URL is not on a verified domain or prefix | Verify the domain or URL prefix in the developer portal |
spam_risk_too_many_posts |
The creator hit the daily cap | Stop; the person can post from the TikTok app |
spam_risk_too_many_pending_share |
5 inbox drafts pending | Wait for the creator to post or discard them |
reached_active_user_cap |
Your app hit its daily creator cap | Wait, or ask for a higher cap |
rate_limit_exceeded (429) |
Over the per-minute limit | Back off |
fail_reason: video_pull_failed / photo_pull_failed |
TikTok could not download the file | Check the URL is public, https, with no redirect |
fail_reason: duration_check_failed, frame_rate_check_failed, picture_size_check_failed, file_format_check_failed |
The file broke a media rule | Re-encode it |
Sources: direct post, upload, query creator info, get post status. spam_risk_user_banned_from_posting, spam_risk_text, spam_risk and auth_removed are final: do not retry them (get post status).
Where Rizamo fits
If you post to a handful of your own accounts, Upload mode or an unaudited app with private accounts may be all you need, and the calls above are the whole job. For an app that serves other people, the audit, the posting screen and the shared cap are the work. Rizamo is a social media API and MCP server built to take that layer off your hands: it reads creator info fresh for each post, asks for every choice TikTok requires with no defaults, follows each post to its final status, and counts each account's daily posts before sending.
Rizamo is opening soon: join the waitlist. What it will support on TikTok: TikTok on Rizamo.