--- name: tiktok-content-posting description: > Upload videos and photo posts to the user's own authorized TikTok account through TikTok's official Content Posting API, with draft upload for in-app editing, Direct Post, exact preview, explicit confirmation, creator-capability checks, commercial and AI-generated disclosures, and status verification. Use whenever someone asks to post, publish, upload, or send a local video, hosted video, or photo carousel to TikTok; create a TikTok draft; check a publish_id; or diagnose Content Posting API authentication, scope, privacy, upload, processing, moderation, or posting failures. Not for trend research, content ideation alone, downloading videos, comments, likes, follows, browser automation, or multi-account engagement. --- # Post content to TikTok Use the bundled CLI to transfer video or photo content through TikTok's official API. Default to a draft that the user finishes in TikTok. Treat both draft upload and Direct Post as external writes: show the exact plan first and require a separate confirmation before sending any media. Run commands from this skill directory: ```bash node scripts/tiktok-post.mjs ``` ## Keep these boundaries - Operate one TikTok account that the user has explicitly authorized. Never add account rotation, proxy rotation, cookie reuse, or browser automation. - Use only media the user owns or is authorized to publish. Do not copy arbitrary content from another platform. - Never put an access token, refresh token, client secret, or complete upload URL in a command, response, audit record, or committed file. Follow [setup.md](references/setup.md) for local credentials. - Never pass `--yes` until the user has seen the current media and exact posting plan and has expressly approved that plan. Execution also requires the `--confirm-plan` ID emitted by that dry-run. A previous general request to "post this" does not replace post-preview confirmation. - Do not silently fall back to a web uploader when an API scope, app review, or account capability blocks the requested operation. Explain the blocker and offer a manual handoff. ## Verify the installed contract without TikTok From this skill directory, run the fixed tokenless evaluation below. It needs no TikTok account, token, or media file, and it must make no network request or filesystem write: ```bash env -u TIKTOK_ACCESS_TOKEN node scripts/tiktok-post.mjs photos \ https://media.example.invalid/launch-1.webp \ https://media.example.invalid/launch-2.webp \ --expected-open-id EVAL_ONLY_NOT_A_REAL_ACCOUNT \ --title 'Launch notes' \ --description 'Two-frame product update.' \ --dry-run ``` Require `dry_run: true`, `executed: false`, `published: false`, `mode: "draft"`, and `media.type: "remote_photos"` with `media.count: 2`. Require `account_info`, `url_reachability`, `url_redirects`, and `remote_media_specs` to be `"unchecked"`; `creator_info` must be `"not_required"`, and `url_ownership` must be `"unchecked_by_cli"`. Stop there: never add `--yes` or `--confirm-plan` to this evaluation. If an independent installed-copy run completes, use the [structured tester form](https://github.com/L4A-ai/tiktok-skills/issues/new?template=tokenless-dry-run-report.yml) only when the reporter independently installed and actually ran `tiktok-content-posting` for their own non-internal purpose and is not a maintainer, a maintainer's teammate, or an internal tester. Use the form's incomplete outcome for install-only or failed attempts, and remove tokens, account identifiers, private URLs, media, and personal paths. This proves only the packaged dry-run contract; it does not prove TikTok authorization, upload, or publication. ## Choose the path | intent | mode | result | |---|---|---| | Let the user add music, effects, or edits in TikTok | `draft` (default) | Sends media to the TikTok inbox; the user finishes and publishes it in TikTok | | Publish with metadata already finalized | `direct` | Asks TikTok to publish after processing and moderation | Draft upload requires an approved and authorized `video.upload` scope. Direct Post requires `video.publish`; draft identity verification also requires `user.info.basic`. Public visibility additionally depends on TikTok's audit of the user's API client. An unaudited client is restricted to a private account and `SELF_ONLY` visibility. Read [policy-and-evidence.md](references/policy-and-evidence.md) before promising public Direct Post. Use a local file for a video that is on the user's device. Use a verified HTTPS URL for media that already lives on a server. Photo posts accept verified HTTPS URLs, not local file upload. See [api-contract.md](references/api-contract.md) for formats, limits, endpoints, and status values. ## Follow the posting workflow ### 1. Diagnose the setup Run `doctor` before the first transfer and whenever authentication or configuration changes: ```bash node scripts/tiktok-post.mjs doctor ``` `doctor` checks the local Node runtime, ffprobe availability, and whether a token is configured; it does not inspect remote app approval or granted scopes. Use [setup.md](references/setup.md), then the read-only `account` call for a draft or `creator` call for Direct Post, to verify the destination. Resolve token expiry and URL-ownership errors from structured API responses. Do not ask the user to paste secrets into chat. ### 2. Inspect the target creator Before a draft, resolve the account bound to the current access token: ```bash node scripts/tiktok-post.mjs account ``` Show its `display_name` and copy the exact `open_id` into `--expected-open-id`. Execution calls the official user-info endpoint again and stops before init if the token now belongs to another account. Before Direct Post, fetch the latest creator capabilities: ```bash node scripts/tiktok-post.mjs creator ``` Show the returned username and nickname so the user can verify the destination account. Copy the exact `creator_username` into `--expected-creator`; execution queries creator info again and stops if the token resolves to a different account. Accept only a privacy value currently returned for that creator. Respect disabled comment, Duet, and Stitch settings, and reject a video longer than the creator's current maximum duration. ### 3. Build a dry-run plan Run `video` or `photos` without `--yes`. The default mode is `draft`: ```bash node scripts/tiktok-post.mjs video ./launch.mp4 \ --expected-open-id '' node scripts/tiktok-post.mjs photos https://media.example.com/1.jpg https://media.example.com/2.jpg \ --expected-open-id '' ``` A dry run performs only local validation: it does not read the token, make network requests, initialize a post, transfer media, or create an inbox draft. Run the separate read-only `account` or `creator` command first, then present the plan together with a viewable preview of the local video or every photo. Let the user edit supplied text and hashtags before proceeding. `--expected-open-id` is required only for draft mode and must come from the latest `account` result. It is a stable app-scoped account identifier, not a secret or access token. For Direct Post, require all of the following on every plan: ```bash node scripts/tiktok-post.mjs video ./launch.mp4 \ --mode direct \ --privacy SELF_ONLY \ --commercial none \ --expected-creator '' ``` - `--privacy` must be an explicit choice from the latest creator response; never invent or default it. - `--commercial` must explicitly declare whether the post is non-commercial (`none`), promotes the creator's own brand (`own`), promotes a third party (`paid`), or both (`both`). Reject incompatible branded-content/privacy combinations. - `--expected-creator` must be the username from the latest `creator` result; never infer it from a stale profile or a media filename. - Do not put `--music-usage-confirmed` or `--branded-content-policy-confirmed` on the dry-run. The output identifies the declarations that must be shown; add each consent flag only after the user accepts the corresponding declaration. - Add `--aigc` when the video is AI-generated or significantly AI-edited so TikTok can apply the creator disclosure label. Do not use that flag merely because an AI agent helped write a caption. - For a remote video, every remote photo, or a local video that ffprobe cannot inspect, verify the applicable format, dimensions, size, frame-rate, and duration constraints outside the CLI and add `--media-specs-confirmed` to both dry-run and execution. Never use it merely to bypass a failed media check. Do not send commercial disclosure, Direct Post interaction, or AIGC flags to a draft endpoint that does not support them. Tell the user to finish those choices in TikTok instead. ### 4. Obtain explicit approval Summarize the plan without changing it: - destination display name plus bound `open_id` for a draft, or creator identity for Direct Post; - media source, SHA-256 for a local video, count, size, duration, and cover choice when applicable; - `draft` or `direct` outcome; - caption or description; - privacy and interaction settings for Direct Post; - commercial and AIGC disclosures; - each declaration listed by the plan and the visible `Promotional content` or `Paid partnership` label when present; - the fact that processing or moderation can delay visibility. Ask the user to confirm this exact plan and retain its `confirmation_id`. If metadata, destination account, local file contents, or media URL changes, generate and show a new dry run. Remote URL contents cannot be cryptographically bound, so call out that limitation. ### 5. Execute the same plan After confirmation, preserve all content-bearing options and add the emitted plan ID plus execution flags: ```bash node scripts/tiktok-post.mjs video ./launch.mp4 \ --expected-open-id '' \ --confirm-plan '' \ --yes node scripts/tiktok-post.mjs video ./launch.mp4 \ --mode direct \ --privacy SELF_ONLY \ --commercial none \ --expected-creator '' \ --music-usage-confirmed \ --confirm-plan '' \ --yes ``` For `--commercial paid` or `both`, also add `--branded-content-policy-confirmed` after the user accepts that policy. The plan ID binds the API payload, expected draft `open_id` or Direct Post creator, remote URLs, and local file SHA-256. The CLI re-checks the destination account and re-hashes and re-opens the same local file before init; either identity or media changes stop before a write. Return the redacted result and `publish_id`. Do not report a successful HTTP response or completed byte upload as a published TikTok post. ### 6. Verify the outcome Poll by the returned identifier: ```bash node scripts/tiktok-post.mjs status '' ``` Interpret the state by mode: - `SEND_TO_USER_INBOX`: draft delivery succeeded; explicitly tell the user it is **not published** and must be completed in TikTok. - `PROCESSING_UPLOAD` or `PROCESSING_DOWNLOAD`: keep the `publish_id` and check later without creating another post. - `PUBLISH_COMPLETE`: the Direct Post completed, or the user completed an inbox draft in TikTok. Public post IDs can remain unavailable until moderation finishes. - `FAILED`: report the structured failure reason and the safe corrective action. If a request times out after initialization, use its returned `publish_id` and status before any retry. Never blindly initialize the same content again; TikTok does not provide a general idempotency key and a retry can create a duplicate. ## Troubleshoot without bypassing controls | symptom | action | |---|---| | `scope_not_authorized` | Reauthorize `user.info.basic` + `video.upload` for a draft, or `video.publish` for Direct Post; do not switch transports | | `access_token_invalid` | Refresh or repeat OAuth through the configured app; never request a token in chat | | draft account mismatch | Run `account` again, show the new destination, and obtain approval for a new plan | | `privacy_level_option_mismatch` | Run `creator` again and ask the user to choose from the current options | | unaudited client rejected | Use the allowed private-account/`SELF_ONLY` test path or use draft mode; do not claim public publishing | | URL ownership rejected | Use a URL under a verified property or a local video with the file-upload path | | pending-share or posting cap | Wait for the stated window or finish existing drafts; do not rotate accounts | | upload URL expired or transfer interrupted | Check status and uploaded bytes first, then follow the API's safe retry rules | Read [setup.md](references/setup.md) for app and OAuth preparation, [api-contract.md](references/api-contract.md) for the wire contract, and [policy-and-evidence.md](references/policy-and-evidence.md) for dated platform constraints.