tiktok

v2026.09.24

Upload videos to the user's TikTok drafts/inbox, where they finish posting in the TikTok app. Use when the user wants to publish or post a generated video to TikTok, or send a video to their TikTok drafts.

GitHub
Install command
npx skhub add acedatacloud/tiktok
Markdown
SKILL.md

Call the TikTok API v2 with curl + jq. The user's OAuth bearer token is in $TIKTOK_TOKEN; every call needs Authorization: Bearer $TIKTOK_TOKEN. Base URL: https://open.tiktokapis.com/v2.

T="https://open.tiktokapis.com/v2"; AUTH="Authorization: Bearer $TIKTOK_TOKEN"

Responses wrap everything in {"data":…,"error":{"code","message","log_id"}}. Check error.code, not just the HTTP status — several real failures (spam_risk_too_many_posts, spam_risk_user_banned_from_posting, reached_active_user_cap) come back as HTTP 200 with a non-ok code. Show error.message verbatim. 401 / access_token_invalid = re-connect TikTok.

Read the account

# Basic profile — open_id identifies the user for every posting call
curl -sS -H "$AUTH" "$T/user/info/?fields=open_id,display_name,avatar_url" \
  | jq '.data.user'

Follower / like counts, bio, and the user's video list need the Display API scopes (user.info.stats, user.info.profile, video.list), which this app has not been granted — don't call /v2/video/list/ or request those fields, they will fail with scope_not_authorized.

Upload the video to the user's drafts

This is the working path. The video lands in the creator's TikTok inbox / drafts; they open the TikTok app to add caption, sound and privacy, then post.

Required order:

  1. Download the exact video locally, but do not upload it yet.
  2. Call request_action_confirmation with kind: "generic", the real video preview, and a summary that says this uploads to drafts rather than publishing.
  3. If cancelled, stop. If confirmed, run exactly one upload: python3 skills/tiktok/scripts/tiktok.py upload video.mp4.
  4. Poll with python3 skills/tiktok/scripts/tiktok.py status PUBLISH_ID until terminal. Never call upload again while polling.

Two source modes. FILE_UPLOAD is the reliable one — PULL_FROM_URL only works from a domain verified in the developer portal, and ours is not verified yet (it returns url_ownership_unverified).

FILE_UPLOAD (default)

Download the video locally first, then init with its exact byte size. Files under 64 MB go up as a single chunk.

SIZE=$(stat -f%z video.mp4 2>/dev/null || stat -c%s video.mp4)

INIT=$(curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --argjson s "$SIZE" \
    '{source_info:{source:"FILE_UPLOAD", video_size:$s, chunk_size:$s, total_chunk_count:1}}')" \
  "$T/post/publish/inbox/video/init/")
echo "$INIT" | jq '{publish_id:.data.publish_id, error:.error.code}'

UPLOAD_URL=$(echo "$INIT" | jq -r '.data.upload_url')
curl -sS -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  -H "Content-Length: $SIZE" \
  -H "Content-Range: bytes 0-$((SIZE-1))/$SIZE" \
  --data-binary @video.mp4 -o /dev/null -w '%{http_code}\n'

The PUT returns 201 when the whole file landed (206 for intermediate chunks). upload_url expires in 1 hour.

PULL_FROM_URL (only from a verified domain)

curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg u "$VIDEO_URL" '{source_info:{source:"PULL_FROM_URL", video_url:$u}}')" \
  "$T/post/publish/inbox/video/init/" | jq '{publish_id:.data.publish_id, error}'

Needs HTTPS and no redirects (any 3xx fails). Domain verification covers subdomains downward only.

Poll status

curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg id "$PUBLISH_ID" '{publish_id:$id}')" \
  "$T/post/publish/status/fetch/" | jq '.data | {status, fail_reason}'

PROCESSING_UPLOAD / PROCESSING_DOWNLOAD → SEND_TO_USER_INBOX = done, the video is waiting in the creator's TikTok inbox. Tell them to open the TikTok app to finish posting, and that processing takes a few minutes.

Don't retry on auth_removed, spam_risk_text, spam_risk, or spam_risk_user_banned_from_posting — those are terminal. internal is retryable.

Direct Post — NOT available yet

Direct Post (/v2/post/publish/video/init/) publishes straight to the profile with a caption and privacy level. Do not call it. It needs a Content Posting API audit this app has not passed, so every attempt fails:

403  unaudited_client_can_only_post_to_private_accounts

The video.publish scope is granted and creator_info/query works, which makes it look available — it is not. Until the audit clears, use the drafts path above.

Gotchas

  • Never hardcode privacy_level if Direct Post is ever enabled. TikTok rejects values outside privacy_level_options and treats a defaulted privacy setting as a guideline violation for the whole app.
  • Rate limits per user token: creator_info/query 20/min, video/init/ 6/min, status/fetch/ 30/min. TikTok also caps posts at roughly 15/day per creator, shared across all apps — another app can exhaust it.
  • SEND_TO_USER_INBOX is success for this flow, not an intermediate state. Don't wait for PUBLISH_COMPLETE — that only happens once the user posts from the app.
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

NOASSERTION

Source path

skills/tiktok

Default branch

main

Latest commit

57cc298

Tree SHA

acaa402