Imported from acedatacloud/skills (
skills/tiktok/SKILL.md). Install upstream withnpx skills add acedatacloud/skills --skill tiktok. Copyright stays with the author (Apache-2.0).
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.
Direct Post to the creator's profile
The production app has passed TikTok's Content Posting API audit. A direct post
publishes to the creator's profile after processing; it is not a draft. Never
choose a privacy level, interaction setting, or disclosure on the creator's
behalf. Never retry an init request while its publish_id is processing.
Required sequence:
-
Identify the exact video and its source. Measure its duration and preview it. A server-hosted video must use a public, non-redirecting HTTPS URL under the verified
acedata.clouddomain (PULL_FROM_URL). A creator-selected device-local MP4 may useFILE_UPLOADand must be no larger than 64 MB. -
Query the selected connection with
python3 skills/tiktok/scripts/tiktok.py creator-info. Checkcreator_usernameagainst the account the creator named. Copy the current nickname, avatar,privacy_level_options, interaction restrictions, and maximum duration intorequest_action_confirmationwithkind: "tiktok.publish"and the real video preview. Do not invent or preselect a privacy option. -
If cancelled, stop. If confirmed, use exactly the card's returned
title,privacy_level,disable_comment,disable_duet,disable_stitch,brand_organic_toggle, andbrand_content_toggle. Addis_aigc: truefor AI-generated content andfalseotherwise. For an Ace Data Cloud brand promotion, the creator must select the own-brand promotional disclosure. Store these values in a temporary JSON file outside the repository. -
Submit once after confirmation, with the exact selected account:
# Server-hosted video on the verified domain: python3 skills/tiktok/scripts/tiktok.py post-url "$VIDEO_URL" \ --duration-sec "$DURATION" \ --expected-creator-username "$CONFIRMED_CREATOR_USERNAME" \ --values-file "$VALUES_JSON" --confirmed # Creator-selected device-local MP4: python3 skills/tiktok/scripts/tiktok.py post-file video.mp4 \ --duration-sec "$DURATION" \ --expected-creator-username "$CONFIRMED_CREATOR_USERNAME" \ --values-file "$VALUES_JSON" --confirmedBoth commands re-query creator info and reject a different account, an unavailable privacy level, disabled interactions, or an overlong video. Never substitute confirmed fields after this check.
-
Poll the returned
publish_idwithstatusuntilPUBLISH_COMPLETEor a terminal failure. Report the actual outcome. An accepted init or successful file transfer is not a published post.
Upload the video to the user's drafts
Use this when the creator explicitly asks for a draft. The video lands in their TikTok inbox / drafts; they open the TikTok app to add caption, sound and privacy, then post.
Required order:
- Identify the exact video and its source: a creator's device-local file or a
server-hosted URL under the verified
acedata.clouddomain. Preview it. - Call
request_action_confirmationwithkind: "generic", the real video preview, and a summary that says this uploads to drafts rather than publishing. - If cancelled, stop. If confirmed, run exactly one upload. For a device-local
file use
python3 skills/tiktok/scripts/tiktok.py upload video.mp4. For a server-hosted video usepython3 skills/tiktok/scripts/tiktok.py upload-url "$VIDEO_URL". - Poll with
python3 skills/tiktok/scripts/tiktok.py status PUBLISH_IDuntil terminal. Never calluploadagain while polling.
Two source modes are available. The developer portal verified acedata.cloud
on 2026-10-03, covering its subdomains. Server-hosted videos on that domain
must use PULL_FROM_URL; use FILE_UPLOAD only for a creator's device-local
file. Verify that the exact URL is public HTTPS and does not redirect.
FILE_UPLOAD (creator device-local file)
Use this only for a file the creator selected from their own device. 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)
python3 skills/tiktok/scripts/tiktok.py upload-url "$VIDEO_URL"
The helper requires public HTTPS under the verified acedata.cloud domain,
video/mp4, and no redirects. Domain verification covers subdomains downward.
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.
Gotchas
- Never preselect
privacy_levelin the confirmation card. The helper checks the creator's selection against freshprivacy_level_optionsbefore making the one Direct Post call. Branded content cannot beSELF_ONLY. - Connections made with the former Sandbox app must be reconnected to the
Production app. If TikTok returns
unaudited_client_can_only_post_to_private_accounts, stop and ask the creator to reconnect; do not retry the same post or change its privacy. - Rate limits per user token:
creator_info/query20/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_INBOXis success for this flow, not an intermediate state. Don't wait forPUBLISH_COMPLETE— that only happens once the user posts from the app.
