tiktok · v2.1 · 2026-08-01 · sha256 08afd198cc166325

tiktok v2.1A

Immutable. This exact content is served forever at /api/v1/blob/08afd198cc166325.

---
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.
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.
connections: [tiktok]
allowed_tools: [Bash]
license: Apache-2.0
metadata:
  author: acedatacloud
  version: "2.1"
---

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.

### 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

- **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.