Skip to main content
Before you start. TikTok gates the Direct Post (video.publish) scope behind a separate App Review. Until that clears, posts go to the upload inbox with privacy=SELF_ONLY and the user manually confirms publish in the TikTok app. Public posting unlocks the moment review is approved — no code change needed; letmepost re-reads the privacy allowlist on every token refresh.

Quick reference

Connect an account

POST /v1/accounts/connect/tiktok. OAuth 2.0 with PKCE. After the callback the provider:
  1. Calls GET /v2/user/info/ to pin platformAccountId to the real TikTok open_id.
  2. Calls POST /v2/post/publish/creator_info/query/ to snapshot the privacy allowlist; accounts with only SELF_ONLY are flagged as auditState: "audit" and the publisher forces SELF_ONLY on every post until that flips.

Scopes

Extended (not requested by default): video.publish (Direct Post), video.list.

Token lifecycle

24-hour access tokens; 365-day refresh tokens that roll on every use. The provider refreshes 1 hour before expiry; subscribe to token.expiring to know when a re-auth window is coming.

Post a video

tiktok-video-post.json

Privacy

privacy accepts three values: public_to_everyone, mutual_follow_friend, self_only. Audit / sandbox accounts always end up as SELF_ONLY regardless of what you pass — letmepost rewrites the request and attaches a tiktok.audit.self_only warning so the caller knows we did so.

Branded content

brandContentToggle: true flags the post as a paid partnership. brandOrganicToggle: true flags it as content for your own brand. TikTok rejects requests that set both — preflight catches it with tiktok.branded_content.mutual_exclusive.

Code samples

How publishing works under the hood

TikTok’s Content Posting API is asynchronous. The publisher:
  1. Calls POST /v2/post/publish/inbox/video/init/ with source: "FILE_UPLOAD", video_size, chunk_size, total_chunk_count. Returns upload_url + publish_id.
  2. PUTs the video bytes to upload_url. Files under 64 MiB go in one request; larger files chunk at 10 MiB with explicit Content-Range headers.
  3. Returns status: "publishing" with the publish_id stamped on cid so the caller can correlate by id.
  4. A BullMQ worker polls POST /v2/post/publish/status/fetch/ on a bucketed schedule (5 s → 30 s → 2 min) until TikTok reaches a terminal state.
  5. Terminal PUBLISH_COMPLETEpost.published webhook with the public TikTok URL. Terminal FAILEDpost.failed with the upstream reason. The 30-minute deadline turns into post.failed with tiktok.publish.pending.
The SEND_TO_USER_INBOX state is also treated as terminal-published — TikTok’s upload-inbox flow IS the documented end-state for audit-mode posts. The user confirms publish in their TikTok app; we have no API signal for that final tap.

Wisdom (platform-specific things that bite)

  • Audit / sandbox accounts can only post SELF_ONLY. letmepost reads the privacy allowlist via creator_info at connect time and on every refresh; SELF_ONLY-only accounts are flagged auditState: "audit" and every post is rewritten regardless of the caller’s intent. A warning fires so you know we did so.
  • The Direct Post (video.publish) scope is a separate App Review. Without it, posts go to the user’s TikTok inbox for manual confirmation. There’s no API signal for the user tapping publish, so the worker treats SEND_TO_USER_INBOX as terminal-published.
  • ffprobe is needed for duration / resolution preflight. Without ffprobe installed locally, letmepost emits a tiktok.video.probe_unavailable warning and lets the upload proceed; TikTok’s own preflight will catch resolution issues at publish time.
  • Aspect ratio 9:16 is strongly preferred. 1:1 and 16:9 work but emit tiktok.video.aspect_non_vertical. Anything else trips tiktok.video.aspect_unusual.
  • Refresh tokens roll on every use. TikTok issues a new refresh_token on each refresh; letmepost stores it. If you bypass the refresh worker and call the upstream endpoint yourself, persist the new refresh token — discarding it silently shortens the next refresh window.
  • push_by_file only. pull_by_url requires media-domain verification on developer.tiktokapis.com which letmepost has not completed; v1 uploads always go through the chunked PUT path.

Common errors

What you can’t do (yet)

  • Photo carousels (unaudited apps).
  • Direct Post (public publishing without user confirmation) until video.publish clears review.
  • pull_by_url uploads (domain verification gate).
  • Reading post engagement, comments, or analytics.
  • TikTok Live, Shop, DMs.

API reference