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)

  1. At developers.tiktok.com, add the Login Kit and Content Posting API products to your app and enable Direct Post.
  2. Register the redirect URI https://social-publish-kappa.vercel.app/api/integrations/tiktok/callback. TikTok requires an absolute https URL with no query string; for local development, point a tunnel at the dashboard and set TIKTOK_REDIRECT_URI to the tunnel URL.
  3. Set TIKTOK_CLIENT_KEY and TIKTOK_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.

SettingEffect
TIKTOK_POST_MODE=inboxFor 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=trueTikTok 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=trueRequests video.list for post analytics. Needs the Display API product on your app.
TIKTOK_REDIRECT_URIOverrides 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

RuleLimit
MediaExactly 1 video, or 1–10 photos (the post limit of 10 media items); no mixing
Video caption2,200 characters
Photo description4,000 characters
Photo titlephotoTitle, 90 characters
Video sizeUp to 1 GB when uploaded by Relaycast (in pull-from-URL mode TikTok fetches the file itself)
Video durationMust not exceed the creator's maximum, which TikTok reports per account
VisibilityprivacyLevel is required and must be one the creator allows
Branded contentbrandContent (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

OptionTypeRequiredDefaultDescription
privacyLevelstring (per account)YesWho can view this post. Options depend on the TikTok account. Unaudited apps can only post as 'Only me'.
disableCommentbooleanNofalseTurn off comments
disableDuetbooleanNofalseTurn off Duet. Videos only.
disableStitchbooleanNofalseTurn off Stitch. Videos only.
coverTimestampMsnumberNoCover frame (ms). Videos only. Timestamp of the frame used as the cover.
brandContentbooleanNofalsePaid partnership. Promoting another brand or a third party. Cannot be used with 'Only me'.
brandOrganicbooleanNofalseYour brand. Promoting yourself or your own business.
isAigcbooleanNofalseAI-generated content
photoTitlestringNoPhoto post title. Photo posts only (max 90 characters). The caption is used as the description.
autoAddMusicbooleanNofalseAdd 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)

Request body
{
  "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

ErrorCause
TikTok needs one video or 1–10 photosNo media attached.
TikTok posts cannot mix photos and videosMixed media.
TikTok posts take exactly one videoMore than one video.
TikTok photo posts require PULL_FROM_URL …Photo post without TIKTOK_PULL_FROM_URL=true.
Choose who can view this TikTok postprivacyLevel is missing.
Branded content (paid partnership) cannot be posted with 'Only me' visibilitybrandContent: true with SELF_ONLY.
Target fails: visibility not available for this accountThe creator (or an unaudited app) doesn't allow that privacyLevel.
Target fails: video longer than the account allowsTrim the video to the creator's maximum duration.
Target fails: url_ownership_unverifiedPull-from-URL mode with a media domain that isn't verified in the TikTok portal.
Target retried: daily posting cap reachedTikTok's per-creator posting cap; retried later.
Login fails with invalid_scopeA requested scope (for example video.list) isn't granted to the app.

This page is also available as Markdown.