tiktok · diff

v1.0 to v2.0

123 added, 34 removed. Audit A to A.

---
name: tiktok
- description: Upload videos to the user's TikTok inbox/drafts and read their TikTok profile via the Content Posting API. Use when the user wants to publish or post a generated video to TikTok, send a video to their TikTok drafts, or check their TikTok account.
+ description: Post videos to the user's TikTok — either published directly (with caption and privacy the user chooses) or uploaded to their drafts/inbox — and read their TikTok profile, stats and video list. Use when the user wants to publish or post a generated video to TikTok, send a video to their TikTok drafts, or check their TikTok account and performance.
when_to_use: |
Trigger when the user wants to push a video (e.g. one generated by
- the video skills) to TikTok. v1 uploads to the user's **inbox /
- drafts** (they finish posting in the TikTok app) — the safe,
- pre-audit path. Confirm before uploading; the video must be a public
- URL on a verified domain (or a file you upload in chunks).
+ the video skills) to TikTok, or to read their TikTok account and
+ post performance. 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.
connections: [tiktok]
allowed_tools: [Bash]
license: Apache-2.0
metadata:
author: acedatacloud
- version: "1.0"
+ version: "2.0"
---
- Call the **TikTok Content Posting API** 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`.
-
- Responses wrap everything in `{"data":...,"error":{"code","message","log_id"}}` —
- `error.code == "ok"` means success; otherwise show `error.message` verbatim.
- `401`/`access_token_invalid` = re-connect TikTok.
+ 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"
- # Profile (account card)
- curl -sS -H "$AUTH" "$T/user/info/?fields=open_id,display_name,avatar_url" \
+ ```
+
+ 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
+ # Profile + stats (needs user.info.profile / user.info.stats)
+ curl -sS -H "$AUTH" "$T/user/info/?fields=open_id,display_name,avatar_url,username,is_verified,follower_count,following_count,likes_count,video_count" \
| jq '.data.user'
```
- ## Upload a video to the user's inbox / drafts (v1)
+ ## List the user's videos
- The simplest path is `PULL_FROM_URL`: TikTok fetches the video from a public URL
- on a **verified domain** (AceData's `cdn.acedata.cloud` is verified for this app).
+ `fields` goes in the **query string**, pagination in the **body** (easy to get
+ wrong). `max_count` max is 20; `cursor` is a **millisecond** UTC timestamp,
+ while each video's `create_time` is in **seconds**.
```bash
- VIDEO_URL="https://cdn.acedata.cloud/...mp4" # must be on 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}'
+ curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
+ "$T/video/list/?fields=id,title,video_description,create_time,duration,cover_image_url,share_url,view_count,like_count,comment_count,share_count" \
+ -d '{"max_count":20}' | jq '{videos:.data.videos, cursor:.data.cursor, has_more:.data.has_more}'
```
- This drops the video into the user's TikTok **inbox** — they open the TikTok app
- to add caption / sound / privacy and post. Poll status:
+ Only **public** videos are returned, newest first.
+ ## 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" \
- -d '{"publish_id":"PUBLISH_ID"}' "$T/post/publish/status/fetch/" \
- | jq '.data | {status, fail_reason}'
+ "$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.
+
+ ### 2. Ask the user to confirm
+
+ Before initializing, the user must explicitly confirm — this is a TikTok
+ requirement, not just good manners:
+
+ - 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."*
+
+ ### 3. Initialize the post
+
+ ```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}'
+ ```
+
+ **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.
+
+ 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 to drafts instead
+
+ When the user would rather finish in the app, use the inbox endpoint — no
+ caption or privacy, and **no `creator_info` call needed**:
+
+ ```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)
+
+ ```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}'
+ ```
+
+ 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.
+
+ 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
- - **v1 = inbox upload only.** True "Direct Post" (set caption + privacy via API,
- posts immediately) needs the `video.publish` scope and a passed Content Posting
- audit — not available until then. Don't call `creator_info` /
- `/post/publish/video/init/` (Direct Post) here; they require that scope.
- - `PULL_FROM_URL` only works from a **domain verified** in the TikTok developer
- app. If the video isn't on a verified domain, use the chunked `FILE_UPLOAD`
- flow instead (init → PUT byte ranges → status).
- - Unaudited apps are rate-limited and may restrict visibility to `SELF_ONLY`.
+ - **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).
+ - 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.