To post a video with TikTok's Content Posting API, get a user access token with the video scope. Call
POST /, then POST / with the post's settings and the
file's size, and PUT the file to the upload_url TikTok returns. POST / tells you when
it's done. Each user token gets 6 init requests a minute, and a creator can make about 15 posts a day through
the API. Until TikTok audits your app, every post is Only me (SELF_ONLY), on private accounts only, for at most
5 users a day.
Checked on November 3, 2026 against TikTok's Direct Post guide, its Direct Post reference, creator info reference, media transfer guide, status reference, sharing guidelines and token guide.
What you need first
A registered app on TikTok for Developers, with the Content Posting API product added and its Direct Post setting turned on.
Approval for the
videoscope. The account that posts must grant it to your app too. To send a draft to the creator's TikTok inbox instead, you need.publish video. TikTok's Content Posting API, explained compares the two routes and the audit..upload A user access token. Send the person to
https://with yourwww .tiktok .com/ v2/ auth/ authorize/ client_key,scope=,video .publish response_type=, yourcode redirect_uriand a randomstate. Then swap the code for tokens from your server:curl -X POST https://open.tiktokapis.com/v2/oauth/token/ \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "client_key=$TIKTOK_CLIENT_KEY" \ --data-urlencode "client_secret=$TIKTOK_CLIENT_SECRET" \ --data-urlencode "code=$CODE" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "redirect_uri=$REDIRECT_URI"The access token lasts 2 hours and the refresh token 180 days. Each refresh returns a new refresh token and ends the old one, so store the new one every time.
A private test account. An unaudited app can only post to accounts set to private.
A video file. MP4 with H.264 is the safe choice. The code below names it
video..mp4
The examples read the token from TIKTOK_ACCESS_TOKEN. Keep it, and your client secret, out of your code and your logs.
Post it
Every answer has an error object. Only "code": "ok" means it worked, even when the HTTP status is 200. The code
below sends the whole file in one chunk, which works for files up to 64 MB. Bigger files need
chunks.
curl
# 1. Ask TikTok what this account can post right now.
curl -X POST https://open.tiktokapis.com/v2/post/publish/creator_info/query/ \
-H "Authorization: Bearer $TIKTOK_ACCESS_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8"
# 2. Start the post. One chunk: chunk_size is the whole file.
SIZE=$(wc -c < video.mp4 | tr -d ' ')
INIT=$(curl -s -X POST https://open.tiktokapis.com/v2/post/publish/video/init/ \
-H "Authorization: Bearer $TIKTOK_ACCESS_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d "{\"post_info\": {\"title\": \"Hello from the TikTok API #test\", \"privacy_level\": \"SELF_ONLY\",
\"disable_comment\": true, \"disable_duet\": true, \"disable_stitch\": true},
\"source_info\": {\"source\": \"FILE_UPLOAD\", \"video_size\": $SIZE, \"chunk_size\": $SIZE,
\"total_chunk_count\": 1}}")
UPLOAD_URL=$(echo "$INIT" | jq -r .data.upload_url)
PUBLISH_ID=$(echo "$INIT" | jq -r .data.publish_id)
# 3. Send the file. --data-binary keeps every byte; plain --data would strip line breaks.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
-H "Content-Range: bytes 0-$((SIZE - 1))/$SIZE" \
--data-binary @video.mp4
# 4. Check on it.
curl -X POST https://open.tiktokapis.com/v2/post/publish/status/fetch/ \
-H "Authorization: Bearer $TIKTOK_ACCESS_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d "{\"publish_id\": \"$PUBLISH_ID\"}"
For a private account, the creator info answer looks like this:
{
"data": {
"creator_username": "yourname",
"creator_nickname": "Your Name",
"creator_avatar_url": "https://…",
"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": 600
},
"error": { "code": "ok", "message": "", "log_id": "202611031200000000000000000000" }
}
privacy_level must be one of privacy_level_options. A private account gets FOLLOWER_OF_CREATOR; a public one
gets PUBLIC_TO_EVERYONE. If comment_disabled, duet_disabled or stitch_disabled is true, send that disable_
flag as true. Don't send a video longer than max_video_post_duration_sec.
The init answer holds the post's publish_id and the upload_url, which works for one hour. TikTok answers the PUT
with 201 Created once it has every byte. The status then moves from PROCESSING_UPLOAD to PUBLISH_COMPLETE, or to
FAILED with a fail_reason.
Python
import os
import time
import requests
API = "https://open.tiktokapis.com"
HEADERS = {
"Authorization": f"Bearer {os.environ['TIKTOK_ACCESS_TOKEN']}",
"Content-Type": "application/json; charset=UTF-8",
}
def call(path, body=None):
resp = requests.post(f"{API}{path}", headers=HEADERS, json=body, timeout=30)
error = resp.json().get("error", {})
if resp.status_code != 200 or error.get("code") != "ok":
raise RuntimeError(f"TikTok answered {resp.status_code}: {error}")
return resp.json()["data"]
creator = call("/v2/post/publish/creator_info/query/")
if "SELF_ONLY" not in creator["privacy_level_options"]:
raise RuntimeError("This account can't post Only me right now.")
with open("video.mp4", "rb") as f:
video = f.read()
size = len(video)
init = call(
"/v2/post/publish/video/init/",
{
"post_info": {
"title": "Hello from the TikTok API #test",
"privacy_level": "SELF_ONLY",
"disable_comment": True,
"disable_duet": True,
"disable_stitch": True,
},
"source_info": {"source": "FILE_UPLOAD", "video_size": size, "chunk_size": size, "total_chunk_count": 1},
},
)
put = requests.put(
init["upload_url"],
headers={"Content-Type": "video/mp4", "Content-Range": f"bytes 0-{size - 1}/{size}"},
data=video,
timeout=300,
)
put.raise_for_status()
while True:
status = call("/v2/post/publish/status/fetch/", {"publish_id": init["publish_id"]})
if status["status"] in ("PUBLISH_COMPLETE", "FAILED"):
break
time.sleep(5)
print(status)
Node.js (18 or later)
Save it as post, so top-level await works.
import { readFile } from 'node:fs/promises';
const API = 'https://open.tiktokapis.com';
const headers = {
Authorization: `Bearer ${process.env.TIKTOK_ACCESS_TOKEN}`,
'Content-Type': 'application/json; charset=UTF-8',
};
async function call(path, body) {
const resp = await fetch(`${API}${path}`, { method: 'POST', headers, body: body && JSON.stringify(body) });
const json = await resp.json();
if (!resp.ok || json.error?.code !== 'ok') throw new Error(`TikTok answered ${resp.status}: ${JSON.stringify(json.error)}`);
return json.data;
}
const creator = await call('/v2/post/publish/creator_info/query/');
if (!creator.privacy_level_options.includes('SELF_ONLY')) throw new Error("This account can't post Only me right now.");
const video = await readFile('video.mp4');
const size = video.length;
const init = await call('/v2/post/publish/video/init/', {
post_info: {
title: 'Hello from the TikTok API #test',
privacy_level: 'SELF_ONLY',
disable_comment: true,
disable_duet: true,
disable_stitch: true,
},
source_info: { source: 'FILE_UPLOAD', video_size: size, chunk_size: size, total_chunk_count: 1 },
});
const put = await fetch(init.upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'video/mp4', 'Content-Range': `bytes 0-${size - 1}/${size}` },
body: video,
});
if (!put.ok) throw new Error(`TikTok answered the upload with ${put.status}: ${await put.text()}`);
let status;
do {
await new Promise((r) => setTimeout(r, 5000));
status = await call('/v2/post/publish/status/fetch/', { publish_id: init.publish_id });
} while (!['PUBLISH_COMPLETE', 'FAILED'].includes(status.status));
console.log(status);
Status reads are limited to 30 a minute per token, so don't poll faster than every 2 seconds.
In a real app, the creator picks the title, who can view it, and whether to allow comments, Duet and Stitch. TikTok's guidelines require that screen, with no privacy level chosen for them and every interaction off until they turn it on. The examples fix those choices only to stay short.
Files over 64 MB: chunks and Content-Range
TikTok's chunk rules are strict, and a wrong number fails the upload:
- A chunk is 5 MB to 64 MB. The last chunk takes the leftover bytes and may be up to 128 MB.
total_chunk_countisvideo_sizedivided bychunk_size, rounded down.- A file under 5 MB goes in one chunk, with
chunk_sizeequal to the file's size. A file over 64 MB must be chunked. - At most 1,000 chunks, sent in order, all to the same
upload_url. - Each
PUTcarriesContent-Range: bytes FIRST-LAST/, with zero-based, inclusive byte positions.TOTAL
TikTok answers 206 for each chunk and 201 after the last one. A 416 means the Content-Range doesn't match what
TikTok has received so far. This plans and sends the chunks:
CHUNK = 10_000_000 # TikTok's own example size
def chunk_plan(size):
if size < CHUNK:
return size, 1 # one chunk: the whole file
return CHUNK, size // CHUNK # the last chunk takes the leftover bytes
def upload_chunks(upload_url, path, size, chunk_size, count):
with open(path, "rb") as f:
for i in range(count):
first = i * chunk_size
last = size - 1 if i == count - 1 else first + chunk_size - 1
part = f.read(last - first + 1)
r = requests.put(
upload_url,
headers={"Content-Type": "video/mp4", "Content-Range": f"bytes {first}-{last}/{size}"},
data=part,
timeout=300,
)
if r.status_code not in (201, 206):
raise RuntimeError(f"Chunk {i + 1} of {count}: TikTok answered {r.status_code}")
Send chunk_size and total_chunk_count from chunk_plan in the init request's source_info. A 50,000,123-byte
file becomes 5 chunks: four of 10,000,000 bytes, then one of 10,000,123.
Hosted videos, inbox drafts, photos
| To do | Do this |
|---|---|
| Post a video already on your server | Send "source_info": {"source": "PULL_FROM_URL", "video_url": "https:// and skip the PUT. The URL must be on a domain or URL prefix you've verified in TikTok's developer portal, use HTTPS and not redirect. TikTok's guidelines say to use this, not FILE_UPLOAD, for files already on your server |
| Send a draft to the creator's inbox | Call POST / with only source_info, using a video token. The creator finishes and posts it in TikTok. The status stops at SEND_TO_USER_INBOX until they do |
| Post photos | Call POST / with "media_type": "PHOTO" and up to 35 image URLs, pulled from your verified domain. See the explainer |
| Stop a slow pull | POST / with the publish_id. TikTok tries, but can't stop one that's almost done |
Limits
| Limit | |
|---|---|
Video init (/ and /) |
6 requests a minute per user token |
Creator info (/) |
20 requests a minute per user token |
Status fetch (/) |
30 requests a minute per user token |
| Posts per creator | Usually about 15 Direct Posts in 24 hours, shared by every app that posts for that creator |
| Unaudited app | Up to 5 users posting in 24 hours, private accounts only, SELF_ONLY only |
| Inbox drafts | At most 5 pending in any 24 hours |
Caption (title) |
2,200, counted in UTF-16 runes |
| Video file | Up to 4 GB. MP4 (recommended), WebM or MOV; H.264 (recommended), H.265, VP8 or VP9 |
| Video picture and frame rate | 360 to 4,096 pixels on each side, 23 to 60 frames a second |
| Video length | 3 minutes for every creator, 5 or 10 for some; check max_video_post_duration_sec |
| Chunks | 5 MB to 64 MB each, the last up to 128 MB, 1 to 1,000 chunks |
upload_url |
Valid for one hour |
| Pull from URL | HTTPS, no redirects, verified domain or prefix; TikTok gives up after one hour |
| Access token, refresh token | 2 hours, 180 days |
Past a rate limit, TikTok answers 429 with rate_limit_exceeded. The daily post cap is a different error:
spam_risk_too_many_posts. If your app hits its daily user cap, you get reached_active_user_cap.
Do it yourself, or let an agent draft it through Flaghand
Call the API yourself when you're building TikTok posting into a product for many creators. TikTok's guidelines expect that: they don't allow an app that only posts to accounts you or your team manage, and they require a posting screen where the creator makes every choice.
If an AI agent writes the posts, Flaghand is a publishing desk for that case. You connect TikTok with TikTok's own
sign-in. Your agent connects to Flaghand's MCP server (https://) or its API and drafts a video (one MP4 or MOV
of up to 64 MB, which Flaghand sends in one chunk) or a photo post of up to 35 photos or slides, then asks you to
approve it. Flaghand reads the video's length and compares it with what TikTok allows the account. To post directly,
you choose on Flaghand's TikTok posting screen: who can view it, with nothing chosen for you; comments, Duet and
Stitch, off unless you turn them on; disclosure; and TikTok's consent statement. A draft for your TikTok inbox takes
those choices in TikTok instead. Your passkey approves that exact post. Right before it goes out, Flaghand asks TikTok
for the account's settings again, and sends nothing if they no longer fit. Flaghand sends it at its time, never twice, and
follows TikTok's status until it's posted, in your inbox or failed. No token Flaghand issues can approve, schedule or
post.
Flaghand's TikTok app isn't audited yet. Until it is, posting directly works only to a private account, seen by Only me. To reach your audience now, have Flaghand send it to your TikTok inbox as a draft, then publish it in the TikTok app. Flaghand and TikTok explains the connection and what's kept, and How to schedule TikTok posts covers TikTok's own scheduler. Sign-up takes no card (current terms). Get started with Flaghand.
Questions
Why can only I see the videos my app posts to TikTok? TikTok hasn't audited your app yet. Until it does, every
Direct Post must be SELF_ONLY, to a private account, for up to 5 users a day. After the audit, the creator must make
the account public and set each old post to Everyone themselves.
What's the difference between video and video? video posts straight to the profile
through /. video sends a draft to the creator's TikTok inbox through
/, and they post it in the app.
How many videos can I post a day through the TikTok API? TikTok says usually about 15 per creator in 24 hours, shared across every app posting for them. The 6 init requests a minute are a rate limit, not a post allowance.
What's the biggest video the TikTok API takes? 4 GB. Send files over 64 MB in chunks of 5 to 64 MB, or let TikTok pull the file from your verified domain.
Is there a TikTok API for Python? The Content Posting API is plain HTTPS and JSON, so requests is enough. The
Python example above posts a video with three endpoints and a PUT.
Why don't I get a TikTok post ID back? TikTok returns publicaly_available_post_id (its spelling) only once a
post is public and has passed moderation. An Only me post never gets one through the API; use the publish_id to
track it.
Sources
Checked November 3, 2026:
- TikTok: Get Started - Direct Post
- TikTok: Direct Post reference
- TikTok: Upload (inbox) reference
- TikTok: Query Creator Info
- TikTok: Media Transfer Guide
- TikTok: Get Post Status
- TikTok: Content Sharing Guidelines
- TikTok: Photo post reference
- TikTok: Login Kit for web, Get a user access token and Manage access tokens