Platforms
TikTok
Publish videos and photo posts to TikTok with the Content Posting API, directly or as inbox drafts.
Overview
The tiktok platform publishes with TikTok's Content Posting API. You can post:
- One video (Direct Post)
- A photo post with 1–10 images (requires pull-from-URL mode; TikTok allows up to 35, but a Relaycast post carries at most 10 media items)
- Drafts sent to the creator's TikTok inbox, when your app has no Direct Post access (inbox mode)
TikTok posts always need media; text-only posts are not supported. The post content becomes the video caption or the photo description.
Connect the account
In Integrations, choose TikTok and approve access.
App setup (self-hosted)
- At developers.tiktok.com, add the Login Kit and Content Posting API products to your app and enable Direct Post.
- Register the redirect URI
https://social-publish-kappa.vercel.app/api/integrations/tiktok/callback. TikTok requires an absolutehttpsURL with no query string; for local development, point a tunnel at the dashboard and setTIKTOK_REDIRECT_URIto the tunnel URL. - Set
TIKTOK_CLIENT_KEYandTIKTOK_CLIENT_SECRET.
Scopes requested: user.info.basic and video.publish (or video.upload in inbox mode), plus video.list when analytics are enabled. TIKTOK_SCOPES (comma-separated) replaces the list entirely.
| Setting | Effect |
|---|---|
TIKTOK_POST_MODE=inbox | For apps without Direct Post. Login asks for video.upload, and posts arrive in the creator's TikTok inbox as drafts to finish in the app. Default: direct. |
TIKTOK_PULL_FROM_URL=true | TikTok fetches media from its URL instead of Relaycast uploading the file. Requires the media domain or URL prefix to be verified in the TikTok developer portal. Required for photo posts. |
TIKTOK_ENABLE_VIDEO_LIST=true | Requests video.list for post analytics. Needs the Display API product on your app. |
TIKTOK_REDIRECT_URI | Overrides the redirect URI (TikTok only). |
Until your app passes TikTok's audit, TikTok only allows posting with Only me visibility (SELF_ONLY) to private accounts. Other visibility levels are rejected by TikTok.
TikTok rejects the whole login with invalid_scope if a requested scope hasn't been granted to the app, so only enable TIKTOK_ENABLE_VIDEO_LIST once the Display API is added.
Post types & media rules
| Rule | Limit |
|---|---|
| Media | Exactly 1 video, or 1–10 photos (the post limit of 10 media items); no mixing |
| Video caption | 2,200 characters |
| Photo description | 4,000 characters |
| Photo title | photoTitle, 90 characters |
| Video size | Up to 1 GB when uploaded by Relaycast (in pull-from-URL mode TikTok fetches the file itself) |
| Video duration | Must not exceed the creator's maximum, which TikTok reports per account |
| Visibility | privacyLevel is required and must be one the creator allows |
| Branded content | brandContent (paid partnership) cannot be used with SELF_ONLY |
Right before posting, Relaycast reads the creator's settings from TikTok. If the creator has turned off comments, Duet or Stitch in TikTok, those are turned off for the post too.
Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
privacyLevel | string (per account) | Yes | Who can view this post. Options depend on the TikTok account. Unaudited apps can only post as 'Only me'. | |
disableComment | boolean | No | false | Turn off comments |
disableDuet | boolean | No | false | Turn off Duet. Videos only. |
disableStitch | boolean | No | false | Turn off Stitch. Videos only. |
coverTimestampMs | number | No | Cover frame (ms). Videos only. Timestamp of the frame used as the cover. | |
brandContent | boolean | No | false | Paid partnership. Promoting another brand or a third party. Cannot be used with 'Only me'. |
brandOrganic | boolean | No | false | Your brand. Promoting yourself or your own business. |
isAigc | boolean | No | false | AI-generated content |
photoTitle | string | No | Photo post title. Photo posts only (max 90 characters). The caption is used as the description. | |
autoAddMusic | boolean | No | false | Add recommended music. Photo posts only. |
privacyLevel has no default: TikTok's posting guidelines require it to be chosen explicitly. The dashboard Playground lists the values allowed for the selected account. Values are PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR and SELF_ONLY. In inbox mode, send SELF_ONLY; the creator picks the real visibility in TikTok. |
disableDuet, disableStitch and coverTimestampMs apply to videos; photoTitle and autoAddMusic apply to photo posts.
Examples
Video
curl -X POST https://social-publish-api.onrender.com/v1/posts \
-H "x-access-key: $RELAYCAST_API_KEY" \
-H "content-type: application/json" \
-d '{
"content": "3 tips for faster mornings #productivity",
"media": [{ "url": "https://cdn.example.com/tips.mp4", "type": "video" }],
"targets": [{
"accountId": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
"options": {
"privacyLevel": "PUBLIC_TO_EVERYONE",
"disableDuet": true,
"coverTimestampMs": 1500,
"isAigc": false
}
}],
"scheduledFor": "2026-11-06T19:00:00Z"
}'const res = await fetch("https://social-publish-api.onrender.com/v1/posts", {
method: "POST",
headers: {
"x-access-key": process.env.RELAYCAST_API_KEY!,
"content-type": "application/json",
},
body: JSON.stringify({
content: "3 tips for faster mornings #productivity",
media: [{ url: "https://cdn.example.com/tips.mp4", type: "video" }],
targets: [
{
accountId: "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
options: { privacyLevel: "PUBLIC_TO_EVERYONE", disableDuet: true, coverTimestampMs: 1500, isAigc: false },
},
],
scheduledFor: "2026-11-06T19:00:00Z",
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://social-publish-api.onrender.com/v1/posts",
headers={"x-access-key": os.environ["RELAYCAST_API_KEY"]},
json={
"content": "3 tips for faster mornings #productivity",
"media": [{"url": "https://cdn.example.com/tips.mp4", "type": "video"}],
"targets": [{
"accountId": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
"options": {
"privacyLevel": "PUBLIC_TO_EVERYONE",
"disableDuet": True,
"coverTimestampMs": 1500,
"isAigc": False,
},
}],
"scheduledFor": "2026-11-06T19:00:00Z",
},
)
data = res.json()["data"]Photo post (requires TIKTOK_PULL_FROM_URL=true)
{
"content": "Weekend hike in four photos",
"media": [
{ "url": "https://media.example.com/hike-1.jpg", "type": "image" },
{ "url": "https://media.example.com/hike-2.jpg", "type": "image" },
{ "url": "https://media.example.com/hike-3.jpg", "type": "image" },
{ "url": "https://media.example.com/hike-4.jpg", "type": "image" }
],
"targets": [{
"accountId": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716",
"options": { "privacyLevel": "FOLLOWER_OF_CREATOR", "photoTitle": "Weekend hike", "autoAddMusic": true }
}],
"publishNow": true
}The photo URLs must be on a domain verified in your TikTok developer portal.
Analytics & comments
With TIKTOK_ENABLE_VIDEO_LIST=true (and the Display API product), analytics show views, likes, comments and shares. Accounts connected before the flag was turned on must reconnect.
Comments and messaging are not supported for TikTok.
TikTok publishes asynchronously and may hold a post for moderation. If TikTok is still processing when Relaycast stops waiting, the target is marked published without an externalUrl (it is never re-sent, to avoid duplicates). Inbox drafts never have a public URL until the creator posts them in TikTok.
Limitations & common errors
| Error | Cause |
|---|---|
TikTok needs one video or 1–10 photos | No media attached. |
TikTok posts cannot mix photos and videos | Mixed media. |
TikTok posts take exactly one video | More than one video. |
TikTok photo posts require PULL_FROM_URL … | Photo post without TIKTOK_PULL_FROM_URL=true. |
Choose who can view this TikTok post | privacyLevel is missing. |
Branded content (paid partnership) cannot be posted with 'Only me' visibility | brandContent: true with SELF_ONLY. |
| Target fails: visibility not available for this account | The creator (or an unaudited app) doesn't allow that privacyLevel. |
| Target fails: video longer than the account allows | Trim the video to the creator's maximum duration. |
Target fails: url_ownership_unverified | Pull-from-URL mode with a media domain that isn't verified in the TikTok portal. |
| Target retried: daily posting cap reached | TikTok's per-creator posting cap; retried later. |
Login fails with invalid_scope | A requested scope (for example video.list) isn't granted to the app. |
This page is also available as Markdown.