tiktok · diff
v2.1 to v2.2
63 added, 72 removed. Audit A to A.
---
name: tiktok
- description: Post videos to the user's TikTok — either published directly (with caption and privacy the user chooses) or uploaded to their drafts/inbox. Use when the user wants to publish or post a generated video to TikTok, send a video to their TikTok drafts, or check which TikTok account is connected.
+ description: 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.
when_to_use: |
Trigger when the user wants to push a video (e.g. one generated by
- the video skills) to TikTok. Direct Post publishes immediately with a
- caption and a privacy level the user picks; Upload drops it into
- drafts for them to finish in the app. Always confirm caption and
- privacy with the user before posting — never post unattended.
+ the video skills) to TikTok. Upload drops it into their drafts and
+ they finish posting in the app — this is the working path. Direct
+ Post (publishing immediately with caption + privacy) requires an
+ audit this app has not passed yet; do not use it.
connections: [tiktok]
allowed_tools: [Bash]
license: Apache-2.0
metadata:
author: acedatacloud
- version: "2.1"
+ version: "2.2"
---
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`.
```bash
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
```bash
# 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 — publish straight to the account
-
- Three steps, in this order. **Step 1 is mandatory** and its result drives what
- you may offer the user.
-
- ### 1. Query creator info (required before every post)
-
- ```bash
- curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
- "$T/post/publish/creator_info/query/" | jq '.data, .error.code'
- ```
-
- Returns `creator_nickname`, `privacy_level_options`, `comment_disabled`,
- `duet_disabled`, `stitch_disabled`, `max_video_post_duration_sec`.
-
- Use it to:
- - **Show the user `creator_nickname`** so they know which account will receive
- the post.
- - **Offer only the returned `privacy_level_options`** — a public account gets
- `PUBLIC_TO_EVERYONE` / `MUTUAL_FOLLOW_FRIENDS` / `SELF_ONLY`; a private one
- gets `FOLLOWER_OF_CREATOR` / `MUTUAL_FOLLOW_FRIENDS` / `SELF_ONLY`.
- - **Reject a too-long video** against `max_video_post_duration_sec`.
- - Not offer comment/duet/stitch toggles that come back disabled.
- - If it returns a spam/cap error, **stop** and tell the user to try later.
+ ## Upload the video to the user's drafts
- ### 2. Ask the user to confirm
+ 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.
+ Confirm with the user before uploading.
- Before initializing, the user must explicitly confirm — this is a TikTok
- requirement, not just good manners:
+ 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`).
- - the **caption** (max 2200 UTF-16 chars; `#tag` and `@mention` work),
- - the **privacy level**, **chosen by them from the options above — never
- default to one, never assume `PUBLIC_TO_EVERYONE`**,
- - and tell them: *"By posting, you agree to TikTok's Music Usage Confirmation."*
+ ### FILE_UPLOAD (default)
- ### 3. Initialize the post
+ Download the video locally first, then init with its exact byte size. Files
+ under 64 MB go up as a single chunk.
```bash
- VIDEO_URL="https://cdn.acedata.cloud/….mp4" # verified-domain public URL
- curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
- -d "$(jq -n --arg t "$CAPTION" --arg p "$PRIVACY" --arg u "$VIDEO_URL" '{
- post_info: {title:$t, privacy_level:$p, is_aigc:true},
- source_info: {source:"PULL_FROM_URL", video_url:$u}}')" \
- "$T/post/publish/video/init/" | jq '{publish_id:.data.publish_id, error}'
- ```
+ SIZE=$(stat -f%z video.mp4 2>/dev/null || stat -c%s video.mp4)
- **Set `is_aigc: true` whenever the video was AI-generated** (anything from our
- video skills). TikTok labels it "Creator labeled as AI-generated" — required
- disclosure, and omitting it risks the post being taken down.
+ 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}'
- Optional `post_info` fields: `disable_comment`, `disable_duet`,
- `disable_stitch`, `video_cover_timestamp_ms`. If the user says the video is a
- paid partnership, set `brand_content_toggle: true` (which cannot be combined
- with `SELF_ONLY`); for promoting their own business use `brand_organic_toggle`.
+ 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'
+ ```
- ## Upload to drafts instead
+ The `PUT` returns **201** when the whole file landed (206 for intermediate
+ chunks). `upload_url` expires in **1 hour**.
- When the user would rather finish in the app, use the inbox endpoint — no
- caption or privacy, and **no `creator_info` call needed**:
+ ### PULL_FROM_URL (only from a verified domain)
```bash
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}'
```
- ## Poll status (both flows)
+ Needs HTTPS and **no redirects** (any 3xx fails). Domain verification covers
+ subdomains downward only.
+ ## Poll status
+
```bash
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, publicaly_available_post_id}'
+ "$T/post/publish/status/fetch/" | jq '.data | {status, fail_reason}'
```
- Status: `PROCESSING_DOWNLOAD` / `PROCESSING_UPLOAD` → `SEND_TO_USER_INBOX`
- (drafts flow) or `PUBLISH_COMPLETE` (direct post), or `FAILED` with
- `fail_reason`. Tell the user processing takes a few minutes.
+ `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`.** TikTok rejects values outside
- `privacy_level_options` (`privacy_level_option_mismatch`) and treats a
- defaulted privacy setting as a guideline violation for the whole app.
- - `publicaly_available_post_id` (TikTok's own spelling — don't "fix" it) is a
- **list**, and is **empty for non-public posts and for public posts still in
- moderation**. Empty ≠ failure.
- - `PULL_FROM_URL` needs the URL's domain verified in the developer app, HTTPS,
- and **no redirects** (any 3xx fails). Verification covers subdomains
- downward only. If the video isn't on a verified domain, use chunked
- `FILE_UPLOAD` (init with `video_size`/`chunk_size`/`total_chunk_count`, then
- `PUT` byte ranges to `upload_url`, which expires in 1 hour).
+ - **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.
+ 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.
+
+