TikTok Content Posting API: publishing videos and photos
The TikTok Content Posting API has two modes. Direct Post (scope video.publish) publishes straight to the creator's profile; Upload (video.upload) sends the video to the creator's TikTok inbox to finish in the app. Before every Direct Post you query creator_info. Until TikTok audits your app, every post is private (SELF_ONLY).
Updated:
Direct Post or Upload: which one do you need?
Use Direct Post when the user finishes the post inside your app: your app collects the caption, privacy and interaction settings, and TikTok publishes right after processing. Use Upload when the creator should edit inside TikTok: your app transfers the media, the creator gets an inbox notification and completes the post in the TikTok app.
Both modes share the same mechanics: an init request returns a publish_id (plus an upload_url for file uploads), the media is transferred, and you track the outcome with the status endpoint or Content Posting webhooks.
Direct Post comes with stricter rules — a mandatory creator info query, a prescribed posting UI and an audit for public visibility — because the content goes live without the creator opening TikTok.
Direct Post vs Upload compared
| Aspect | Direct Post | Upload (inbox) |
|---|---|---|
| Scope | video.publish | video.upload |
| Video endpoint | /v2/post/publish/video/init/ | /v2/post/publish/inbox/video/init/ |
| Photo endpoint | /v2/post/publish/content/init/, post_mode = DIRECT_POST | /v2/post/publish/content/init/, post_mode = MEDIA_UPLOAD |
| Who sets caption and privacy | Your app, from the user's explicit choices | The creator, inside TikTok |
| Creator info query | Required before each post | Not part of the documented flow |
| Volume limit | Per-creator cap per 24 hours, typically around 15 posts | At most 5 pending shares within any 24 hours |
| Final status | PUBLISH_COMPLETE | SEND_TO_USER_INBOX, then the creator posts |
| Socialift SDK | publish_video, publish_photos | upload_video, publish_photos(draft=True) |
How to publish a video with Direct Post
Order matters: TikTok's guidelines require the creator info query and a specific posting UI before the init call.
Get
video.publishauthorizationThe creator must have authorized your app for
video.publishthrough Login Kit. Keep the access token fresh — it lives 24 hours.Query creator info
POST
/v2/post/publish/creator_info/query/returns the creator's nickname and avatar,privacy_level_options,comment_disabled,duet_disabled,stitch_disabledandmax_video_post_duration_sec. Errors such asspam_risk_too_many_postsorreached_active_user_capmean you should not offer posting right now.Show a compliant posting screen
Display the creator's nickname, a privacy dropdown with no default value, interaction toggles that are off by default (greyed out if the creator disabled them), a commercial content disclosure switch, a preview and the Music Usage Confirmation consent. Tell users processing can take a few minutes.
Initialize the post
Send
post_info(title, privacy_level, disable_comment, disable_duet, disable_stitch, branded content toggles) andsource_infowithFILE_UPLOADorPULL_FROM_URL. The response containspublish_idand, for file uploads,upload_url.Transfer the media
For
FILE_UPLOAD, PUT the chunks toupload_urlwithin one hour. ForPULL_FROM_URL, TikTok downloads the file from your verified URL itself.Track the result
Poll
/v2/post/publish/status/fetch/with thepublish_idor subscribe to Content Posting webhooks, and show the final outcome to the user.
privacy_level values
Send only a value that creator info returned for this creator — anything else is rejected with privacy_level_option_mismatch.
| Value | Who can see the post |
|---|---|
PUBLIC_TO_EVERYONE | Everyone |
MUTUAL_FOLLOW_FRIENDS | Friends — accounts that follow each other |
FOLLOWER_OF_CREATOR | The creator's followers |
SELF_ONLY | Only the creator; the only value an unaudited client can use |
The list depends on the creator's account settings, so never hard-code it.
Direct Post in Python
The SDK sends the init request, uploads file chunks and, with wait=True, polls the status until the post is published or fails.
from socialift import TikTok
async def post_video(token: str, privacy: str, caption: str) -> dict:
async with TikTok.from_env() as tt:
info = await tt.creator_info(token)
if privacy not in info["privacy_level_options"]:
raise ValueError(f"{privacy} is not allowed for this creator")
return await tt.publish_video(
token,
video="renders/launch.mp4", # FILE_UPLOAD, chunked by the SDK
title=caption,
privacy_level=privacy, # SELF_ONLY until the app is audited
disable_comment=info.get("comment_disabled", False),
disable_duet=info.get("duet_disabled", False),
disable_stitch=info.get("stitch_disabled", False),
wait=True,
)publish_video defaults to SELF_ONLY, the only level an unaudited client may use. wait=True polls the status every 3 seconds for up to 2 minutes and raises ApiError carrying TikTok's fail_reason if the post fails.
Passing video_url= instead of video= switches to PULL_FROM_URL. If the domain is not verified, the SDK raises PermissionError for url_ownership_unverified.
What unaudited apps can and cannot do
TikTok restricts every Content Posting API client that has not passed its audit. All content it posts is limited to private viewing (SELF_ONLY), creator accounts must be set to private at the time of posting, and no more than 5 users can post through it in a 24-hour window. Trying to post publicly returns unaudited_client_can_only_post_to_private_accounts.
That is enough to build and demo an integration, but not to launch it. After testing you apply for the audit, which checks compliance with TikTok's terms and the content sharing guidelines. The sandbox does not help here: it does not offer Content Posting API access for public videos.
Posts created before the audit stay private. To make one public, the account owner first switches the account to public and then changes that post's privacy to Everyone in TikTok.
PULL_FROM_URL vs FILE_UPLOAD
Both sources work for Direct Post and Upload videos. Photo posts only support PULL_FROM_URL.
| Aspect | PULL_FROM_URL | FILE_UPLOAD |
|---|---|---|
| How it works | TikTok downloads the media from your URL | You PUT the bytes to the returned upload_url |
| Prerequisite | Domain or URL prefix verified in URL properties | None |
| Address rules | HTTPS, no redirects, a domain rather than an IP; the download times out one hour after it starts, so keep the URL reachable | upload_url is valid for one hour |
| Chunking | Not applicable | Chunks of 5–64 MB, last chunk up to 128 MB; files under 5 MB go in one piece; up to 1000 chunks, in order |
| Chunk count | Not applicable | total_chunk_count = video_size ÷ chunk_size, rounded down |
| Header | Not applicable | Each chunk carries Content-Range: bytes first-last/total |
| Typical error | url_ownership_unverified | HTTP 403 when upload_url has expired |
| Socialift SDK | video_url=, photo_urls= | video= (bytes or file path), optional chunk_size= |
Notes and limits
URL prefix verification is literal: verifying https://example.com/videos/user/ covers https://example.com/videos/user/123/a.mp4 but not https://example.com/videos/2023/user/123/a.mp4.
Media requirements
| Media | Requirement |
|---|---|
| Video container | MP4 (recommended), WebM, MOV |
| Video codec | H.264 (recommended), H.265, VP8, VP9 |
| Frame rate | 23–60 FPS |
| Resolution | 360–4096 px on each side |
| File size | Up to 4 GB |
| Duration | Up to max_video_post_duration_sec from creator info |
| Video title | Up to 2200 UTF-16 runes |
| Photos | WebP or JPEG, up to 1080p, up to 20 MB each, up to 35 images per post |
| Photo post title / description | Up to 90 / 4000 UTF-16 runes |
Photo posts, drafts and status checks
Photos always come from verified URLs. Inbox uploads end in SEND_TO_USER_INBOX, so check them with publish_status instead of waiting for publication.
from socialift import TikTok
async def carousel_and_draft(token: str) -> None:
async with TikTok.from_env() as tt:
# creator_info() before Direct Post, as in the video example
carousel = await tt.publish_photos(
token,
photo_urls=[
"https://media.example.com/tiktok/look-1.jpg",
"https://media.example.com/tiktok/look-2.jpg",
],
title="Autumn lookbook",
description="Two looks from the new collection",
privacy_level="SELF_ONLY",
cover_index=0,
wait=True,
)
print(carousel["status"]["status"]) # PUBLISH_COMPLETE
draft = await tt.upload_video(token, video="renders/raw-cut.mp4")
state = await tt.publish_status(draft["publish_id"], token)
print(state["status"]) # PROCESSING_UPLOAD or SEND_TO_USER_INBOXpublish_photos(..., draft=True) sends photos to the creator's inbox in MEDIA_UPLOAD mode; that requires the video.upload scope.
Post status values
| Status | Meaning |
|---|---|
PROCESSING_UPLOAD | File chunks are being uploaded (FILE_UPLOAD only) |
PROCESSING_DOWNLOAD | TikTok is downloading from your URL (PULL_FROM_URL only) |
SEND_TO_USER_INBOX | Upload mode: the notification reached the creator's inbox |
PUBLISH_COMPLETE | The content was posted |
FAILED | Processing stopped; see fail_reason, e.g. file_format_check_failed, duration_check_failed, frame_rate_check_failed, video_pull_failed, spam_risk_too_many_posts |
publicaly_available_post_id (spelled that way in the API) is returned only once the post is public and approved; private posts have no public ID.
Production checklist for TikTok posting
- Query creator info before every Direct Post and build the privacy dropdown from
privacy_level_options. - Leave privacy and interaction settings without defaults; grey out whatever the creator disabled.
- Stay under 6 init requests and 30 status requests per minute per user token.
- Treat
spam_risk_too_many_postsandspam_risk_too_many_pending_shareas a pause, not a retry loop. - Never superimpose logos, watermarks, links or promotional text on content shared to TikTok.
- Keep media URLs on a verified HTTPS prefix without redirects.
- Log the
log_idfrom every error response for TikTok support.
Frequently asked questions
Can I publish public TikTok videos via the API without an audit?
No. An unaudited client can only post with SELF_ONLY visibility, the creator's account must be private at posting time, and up to 5 users can use it in 24 hours. Public posting requires your app to pass TikTok's audit.
Why does TikTok return url_ownership_unverified?
In PULL_FROM_URL mode the media URL must belong to a domain or URL prefix verified under URL properties in the TikTok developer portal. Verify the domain, or upload the file with FILE_UPLOAD, which needs no verification.
How many posts per day can the Content Posting API publish?
Direct Post has a per-creator cap per 24 hours — typically around 15 posts, varying by creator. Hitting it returns spam_risk_too_many_posts. In Upload mode there can be at most 5 pending shares within any 24 hours.
Do I have to call creator_info before every post?
Yes. TikTok's rules require fetching the creator's latest information before a Direct Post: allowed privacy levels, disabled interactions and the maximum video duration can change at any time.
Can I upload photos as files?
No. Photo posts only accept PULL_FROM_URL, so images are fetched from HTTPS links on a verified domain. A single post can contain up to 35 images.
Can my app add a logo or watermark to the video?
No. TikTok's guidelines forbid apps and integrations from superimposing a brand name, logo, watermark, link or promotional text on content shared to TikTok.