What do you need before your first post?
From TikTok's getting started guide:
- A registered app on TikTok for Developers.
- The Content Posting API product added to it, with the Direct Post setting switched on if you want to publish directly.
- Approval for the
video.publish scope, plus each creator's consent to it during OAuth. Upload-to-inbox uses a different scope, video.upload.
- The creator's access token and open ID.
- Media: a video file you upload, or a URL on a domain or URL prefix you have verified with TikTok. Photos can only be sent as URLs on a verified domain or prefix.
For the OAuth step itself the app also needs Login Kit, and creating the app asks for terms of service and privacy policy URLs.
Direct Post or upload to inbox: which flow should you use?
The API has two ways in, and they end in different places.
|
Direct Post |
Upload to inbox |
| Scope |
video.publish |
video.upload |
| Init endpoint |
/v2/post/publish/video/init/ |
/v2/post/publish/inbox/video/init/ |
| What happens |
The video is published with the caption and settings you send |
The video lands as a draft; the creator gets an inbox notification and finishes the post in TikTok |
| Who controls caption and privacy |
Your app, via the creator's choices in your UI |
The creator, inside TikTok |
| Extra limit |
Daily post cap per creator |
At most 5 pending shares per 24 hours |
The upload-to-inbox reference says users "must click on inbox notifications to continue the editing flow in TikTok and complete the post." Use it when a person reviews every post in TikTok anyway, for example an editing tool that hands off a finished cut. Use Direct Post for scheduling, automation and anything that should go live without someone opening the app.
How does a Direct Post work, step by step?
Four calls, all on https://open.tiktokapis.com:
1. Query creator info. POST /v2/post/publish/creator_info/query/ (see the next section).
2. Initialize the post. POST /v2/post/publish/video/init/ with post_info and source_info, per the Direct Post reference:
curl --location 'https://open.tiktokapis.com/v2/post/publish/video/init/' \
--header 'Authorization: Bearer act.example12345Example12345Example' \
--header 'Content-Type: application/json; charset=UTF-8' \
--data-raw '{
"post_info": {
"title": "this will be a funny #cat video on your @tiktok #fyp",
"privacy_level": "MUTUAL_FOLLOW_FRIENDS",
"disable_duet": false,
"disable_comment": true,
"disable_stitch": false,
"video_cover_timestamp_ms": 1000
},
"source_info": {
"source": "FILE_UPLOAD",
"video_size": 50000123,
"chunk_size": 10000000,
"total_chunk_count": 5
}
}'
privacy_level is required and must be one of the values creator_info returned.
title is the caption, up to 2,200 UTF-16 runes.
brand_content_toggle, brand_organic_toggle and is_aigc are the commercial-content and AI-generated disclosures.
source is FILE_UPLOAD (you send the bytes) or PULL_FROM_URL (TikTok downloads from video_url).
The response gives you a publish_id and, for FILE_UPLOAD, an upload_url that is valid for one hour.
3. Upload the file. PUT the bytes to upload_url in order, one chunk per request, with Content-Type, Content-Length and Content-Range: bytes {FIRST}-{LAST}/{TOTAL}. The media transfer guide sets the rules:
- Chunks are 5 MB to 64 MB. The last one can be up to 128 MB to take the remainder.
- Files under 5 MB go up as a single chunk. Files over 64 MB must be split.
- 1 to 1,000 chunks, and
total_chunk_count is the size divided by the chunk size, rounded down.
- Video: MP4 (recommended), WebM or MOV; H.264 (recommended), H.265, VP8 or VP9; 23–60 FPS; 360–4,096 px on each side; up to 4 GB.
With PULL_FROM_URL there's nothing to upload, but the URL must be HTTPS, must not redirect, and must stay reachable for the download, which times out an hour after it starts.
4. Check the status. POST /v2/post/publish/status/fetch/ with the publish_id. Per the status reference, you'll see PROCESSING_UPLOAD or PROCESSING_DOWNLOAD, then PUBLISH_COMPLETE or FAILED (inbox uploads also pass through SEND_TO_USER_INBOX). publicaly_available_post_id (TikTok's spelling) is only filled in once the post is public and has passed moderation. A private post from an unaudited app won't have one.
Photo posts follow the same pattern through POST /v2/post/publish/content/init/, with post_mode and media_type in the body and photo URLs on a verified domain or prefix.
What does creator_info return, and why is it required?
POST /v2/post/publish/creator_info/query/ returns the current state of the account you are about to post to. From the creator_info reference:
| Field |
Meaning |
creator_username, creator_nickname, creator_avatar_url |
Who the post will go to. The avatar URL expires after 2 hours |
privacy_level_options |
The privacy levels this creator can use right now |
comment_disabled, duet_disabled, stitch_disabled |
Interactions the creator has turned off (duet and stitch are also off for private accounts) |
max_video_post_duration_sec |
The longest video this creator may post |
It also tells you when a creator can't post at all: spam_risk_too_many_posts (they've hit the daily cap), spam_risk_user_banned_from_posting, or reached_active_user_cap (your app has used its daily quota of active posting users).
You can't cache this. Privacy options and the duration limit change when the creator changes their account settings, and TikTok's content sharing guidelines build your posting screen from it.
What privacy levels can a TikTok API post use?
The values depend on whether the creator's account is public or private:
- Public accounts:
PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, SELF_ONLY
- Private accounts:
FOLLOWER_OF_CREATOR, MUTUAL_FOLLOW_FRIENDS, SELF_ONLY
Send a value that isn't in privacy_level_options, or leave it out, and init fails with privacy_level_option_mismatch. Branded content can't be set to private.
What can an unaudited app do?
Not much in public. TikTok's getting started guide is blunt: "All content posted by unaudited clients will be restricted to private viewing mode." The Direct Post reference adds the error you'll hit: unaudited_client_can_only_post_to_private_accounts, "Unaudited clients can only post to a private account. The publish attempt will be blocked when calling /publish/video/init/."
To lift that, your app goes through TikTok's audit, which checks compliance with its terms and the content sharing guidelines. Those guidelines are UI requirements, and an app that skips them gets rejected:
- Show the creator's nickname on the posting screen so the user knows which account they're posting to.
- Privacy level is picked manually from a dropdown, with no default value.
- Comment, Duet and Stitch start unchecked. If the creator has disabled one in TikTok, grey it out.
- The commercial content toggle is off by default. When it's on, the user must pick "Your brand" and/or "Branded content" before the publish button enables.
- Show the consent declaration (linking TikTok's Music Usage Confirmation, plus the Branded Content Policy for commercial posts) before the publish button.
- Show a preview, let users edit any preset caption or hashtags, and don't add your own watermark or logo.
- Tell users it may take a few minutes for the post to appear on their profile.
- Only send content after the user has expressly agreed to the upload.
Read that list again if you plan fully automated posting with no human in the loop. The guidelines assume a person picks the privacy level for each post.
What are the TikTok Content Posting API rate limits?
All per user access token, from TikTok's reference pages:
| Endpoint |
Limit |
creator_info/query |
20 requests per minute |
video/init (Direct Post) |
6 requests per minute |
inbox/video/init |
6 requests per minute |
status/fetch |
30 requests per minute |
Going over returns HTTP 429 rate_limit_exceeded. On top of that:
- Daily post cap per creator.
spam_risk_too_many_posts means "the daily post cap from the API is reached for the current user." TikTok doesn't publish the number.
- Daily active-user cap per app.
reached_active_user_cap means your client has hit its daily quota of active publishing users. That number isn't published either.
- Pending inbox shares. At most 5 in any 24 hours, then
spam_risk_too_many_pending_share.
Because the daily caps aren't published, treat both errors as "try again tomorrow" and show the user a clear message. Retrying in a loop won't help.
How do you post to TikTok with Bulkit?
Bulkit doesn't get around the audit. TikTok sets the private-only rule, and it applies to the app, not to whoever sends the request. With Bulkit you create your own TikTok app with Login Kit and the Content Posting API (Direct Post on), add the user.info.basic, video.publish and video.list scopes, and paste its client key and secret into Bulkit. Until review, only accounts you add as test users in the app's Sandbox tab can connect, and their posts are private. After approval, TikTok runs the audit, and once it passes, posts can be public. The TikTok app guide walks through each step, including verifying Bulkit's URL prefix for photo posts.
What you don't have to write: the creator_info calls, chunked uploads, status polling, and the privacy-level checks. From the TikTok platform page:
- Two post types via
channels[].postType: video (default, exactly one MP4 or MOV) and photo (1 to 35 pictures, the first is the cover).
- Captions up to 2,200 characters for video, 4,000 for photo descriptions. Photo posts take an optional 90-character
title.
privacy_level is required. Read the values this account allows from GET /channels/{id}/capabilities. That is creator_info, served through Bulkit.
- The other options:
allow_comment, allow_duet, allow_stitch, video_cover_timestamp_ms, disclose_content, brand_organic_toggle, brand_content_toggle, is_aigc, and auto_add_music (photo only). True/false values are sent as the strings "true" and "false".
With the Node SDK, check the account's privacy choices, upload the video, then post:
import { openAsBlob } from "node:fs";
import { Bulkit } from "bulkit";
const bulkit = new Bulkit(); // reads BULKIT_API_KEY
// creator_info, via Bulkit: which privacy levels can this account use?
const caps = await bulkit.channels.capabilities("CHANNEL_TIKTOK");
const video = await bulkit.media.uploadMultipart(await openAsBlob("launch.mp4"), {
fileName: "launch.mp4",
mimeType: "video/mp4",
});
const post = await bulkit.posts.create({
intent: "now",
channels: [
{
channelId: "CHANNEL_TIKTOK",
options: {
privacy_level: "PUBLIC_TO_EVERYONE",
allow_comment: "true",
allow_duet: "false",
allow_stitch: "false",
},
blocks: [{ content: "Behind the scenes 🎬", mediaIds: [video.id] }],
},
],
});
In caps.options, the privacy_level entry lists the values this account allows in choices. Whatever it lists, an unaudited app is still held to private posts. Bulkit's docs put it this way: until TikTok approves your app, it "only allows SELF_ONLY (private) posts, whatever you send."
Two TikTok limits carry through unchanged: apps can't delete TikTok posts, and TikTok doesn't give apps access to comments. Analytics (views, likes, comments, shares) are returned for published posts. Publishing is asynchronous, so intent: "now" returns publishing. Check posts.retrieve(post.id) or a webhook for the result.
Native or Bulkit?
The audit and the UI requirements are the same either way. If TikTok is your only network and you're building the posting screen anyway, the native API is four endpoints and a chunked upload. That's manageable.
If TikTok is one of several networks, Bulkit lets you send the same request to all of them, at $4.99 a month with 500 actions included, then $0.01 per action. The TikTok channel page lists everything supported. To let an AI agent schedule TikTok posts, connect it over MCP. To compare Bulkit with other unified APIs, see the comparisons.