tiktok

SkillMedia

Publish organic TikTok content (videos, photos, carousels) through the TikTok-compliant interactive posting form via the Hyper MCP. Use when the user wants to post a video to TikTok, share photos on TikTok, upload to TikTok, or any phrase like "post this to TikTok" / "share on TikTok" / "put this on

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the tiktok skill

What this skill tells your AI

The instructions your AI receives, as published by hyperfx-ai/marketing-skills in skills/tiktok/SKILL.md and read by ahel’s review.

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.

Upload the video to the user's drafts

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.

Required order:

  1. Download the exact video locally, but do not upload it yet.
  2. Call request_action_confirmation with kind: "generic", the real video preview, and a summary that says this uploads to drafts rather than publishing.
  3. If cancelled, stop. If confirmed, run exactly one upload: python3 skills/tiktok/scripts/tiktok.py upload video.mp4.
  4. Poll with python3 skills/tiktok/scripts/tiktok.py status PUBLISH_ID until terminal. Never call upload again while polling.

Two source modes. FILE_UPLOAD is the reliable onePULL_FROM_URL only works from a domain verified in the developer portal, and ours is not verified yet (it returns url_ownership_unverified).

FILE_UPLOAD (default)

Download the video locally first, then init with its exact byte size. 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)

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}'

Needs HTTPS and no redirects (any 3xx fails). Domain verification covers subdomains downward only.

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_DOWNLOADSEND_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 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 — 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.

Signals

GitHub stars
86
Forks
15
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
tiktok
Source
github.com/hyperfx-ai/marketing-skills