Quick reference
| Field | Value |
|---|
| Character limit | 2,200 (caption) |
| Photo title | 90 chars, photo posts only |
| Photos per post | 1–35 (slideshow) |
| Videos per post | 1 |
| Video formats | MP4, MOV, WebM |
| Photo formats | JPEG, PNG, WebP |
| Max video size | 500 MB |
| Max photo size | 20 MB per image |
| Video duration | 3–600 seconds |
| Photo aspect ratio | 9:16 recommended (auto-cropped otherwise) |
| Post types | Video, Photo Carousel |
| Scheduling | ✅ Schedule + post now |
| First comment | ❌ Not exposed by TikTok’s API |
| Branded content | ✅ Disclosure toggles |
Before you start
TikTok has a strict review process before a TikTok App can publish on behalf of arbitrary creators. While in sandbox mode, every post is forced to privacy: "SELF_ONLY" regardless of what your request sends. The full set of privacy values unlocks after TikTok approves the app.If you’re seeing your scheduled posts appear “only to me” on TikTok, your Postbreeze server’s TikTok credentials are still in sandbox.
Before scheduling, the connected account must:
- Have completed the OAuth connect flow at least once.
- Have granted
video.publish, video.upload, and user.info.basic scopes.
- Be on a region/account type TikTok permits API publishing for — Personal, Creator, and Business accounts all work; advertiser-only sub-accounts do not.
TikTok rejects posts whose privacy doesn’t match the per-creator allow-list returned by the creator-info API. Postbreeze validates this when you connect the account — if your account can’t post PUBLIC_TO_EVERYONE, the field’s enum will be filtered down for you.
See also: Platform settings — TikTok and Media uploads.
Quick start
Workspace is inferred from your API key — no workspaceId argument.
Use the flat shape (content + platforms) for the common case;
drop to the nested shape when you’d
rather group every per-target field under targets[].
TikTok requires privacy on every post — there is no default. Pass
one of PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS,
FOLLOWER_OF_CREATOR, or SELF_ONLY on every TikTok target.
import Postbreeze from "@postbreeze/node";
const postbreeze = new Postbreeze({ apiKey: process.env.POSTBREEZE_API_KEY });
const post = await postbreeze.posts.create({
content: "Behind the scenes from launch week ✨",
scheduledFor: new Date(Date.now() + 60_000).toISOString(),
mediaIds: ["med_video_…"],
platforms: [
{
accountId: "soc_tiktok_…",
platformOptions: {
platform: "TIKTOK_PERSONAL",
privacy: "PUBLIC_TO_EVERYONE",
allowComments: true,
allowDuet: false,
allowStitch: false,
autoAddMusic: false,
brandOrganicToggle: false,
brandContentToggle: false,
},
},
],
});
console.log("Scheduled:", post.id);
Content types
Video post
A single video. The publisher uploads the file to TikTok, polls publish/status/fetch until the platform reports PUBLISH_COMPLETE, then returns the TikTok video id on the PostTarget.externalPostId.
const post = await postbreeze.posts.create({
content: "Quick tutorial #1",
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
mediaIds: ["med_video_…"],
platforms: [{
accountId: "soc_tiktok_…",
platformOptions: {
platform: "TIKTOK_PERSONAL",
privacy: "PUBLIC_TO_EVERYONE",
allowComments: true,
allowDuet: true,
allowStitch: true,
autoAddMusic: false,
brandOrganicToggle: false,
brandContentToggle: false,
},
}],
});
Photo carousel
Up to 35 images stitched into a TikTok slideshow. The first item in mediaIds becomes the cover image — reorder via the media tray in compose, or by sending the array in the order you want.
const post = await postbreeze.posts.create({
content: "Trip recap — best moments from the weekend",
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
mediaIds: ["med_photo_1", "med_photo_2", "med_photo_3"],
platforms: [{
accountId: "soc_tiktok_…",
platformOptions: {
platform: "TIKTOK_PERSONAL",
privacy: "PUBLIC_TO_EVERYONE",
allowComments: true,
allowDuet: false,
allowStitch: false,
autoAddMusic: true,
brandOrganicToggle: false,
brandContentToggle: false,
photoTitle: "Trip recap",
},
}],
});
Branded content disclosure
Branded content (paid partnerships) requires the brandContentToggle: true flag. TikTok enforces that branded posts must be privacy: "PUBLIC_TO_EVERYONE" — sending any other privacy value alongside brandContentToggle: true is rejected by Postbreeze’s validator before the request reaches TikTok.
await postbreeze.posts.create({
content: "Loving the new gear from @brand — link in bio 🎒 #ad",
scheduledFor: new Date(Date.now() + 60_000).toISOString(),
mediaIds: ["med_video_…"],
platforms: [{
accountId: "soc_tiktok_…",
platformOptions: {
platform: "TIKTOK_PERSONAL",
privacy: "PUBLIC_TO_EVERYONE", // required when brandContentToggle is true
allowComments: true,
allowDuet: true,
allowStitch: true,
autoAddMusic: false,
brandOrganicToggle: false,
brandContentToggle: true, // "Branded content" disclosure
},
}],
});
Images
| Property | Requirement |
|---|
| Max photos | 35 per carousel |
| Formats | JPEG, PNG, WebP |
| Max file size | 20 MB per image |
| Aspect ratio | 9:16 recommended |
| Resolution | Auto-resized to 1080 × 1920 px |
Videos
| Property | Requirement |
|---|
| Videos per post | 1 |
| Formats | MP4, MOV, WebM |
| Max file size | 500 MB |
| Min duration | 3 seconds |
| Max duration | 10 minutes |
| Aspect ratio | 9:16 (any other ratio is letter-boxed by TikTok) |
| Resolution | 1080 × 1920 recommended |
| Codecs | H.264 |
| Frame rate | 30 fps recommended |
You can’t mix photos and videos in the same post. Use either all-photos (carousel) or one video.
See Media uploads for the presign + upload flow that produces mediaIds.
TikTok settings go in platformOptions on each entry of platforms[]. The platform discriminator is required on every TikTok target — use "TIKTOK_PERSONAL" for Personal/Creator accounts and "TIKTOK_BUSINESS" for Business accounts. Both branches share the same field shape.
| Field | Type | Required | Description |
|---|
platform | "TIKTOK_PERSONAL" | "TIKTOK_BUSINESS" | Yes | Discriminator. Picks the schema branch. |
privacy | enum | Yes | PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, or SELF_ONLY. No default — must be set explicitly to honor TikTok’s Content Sharing Guidelines. |
allowComments | boolean | No (default false) | Enable or disable comments. |
allowDuet | boolean | No (default false) | Enable or disable Duets on this post. |
allowStitch | boolean | No (default false) | Enable or disable Stitches on this post. |
autoAddMusic | boolean | No (default false) | Photo posts only. TikTok auto-adds a popular track when true. |
brandOrganicToggle | boolean | No (default false) | “Your Brand” disclosure — content promotes the creator’s own brand. Surfaced to TikTok as brand_organic_toggle. |
brandContentToggle | boolean | No (default false) | “Branded Content” disclosure — paid partnership. Surfaced as brand_content_toggle. Forces privacy: "PUBLIC_TO_EVERYONE" — any other value is rejected. |
photoTitle | string | No | Photo posts only. Short metadata title (≤ 90 UTF-16 runes). Ignored for video posts. |
firstComment is not accepted on TikTok targets — TikTok’s API has no comment-reply endpoint, so Postbreeze can’t auto-post a follow-up reply.
Photo carousel caption behavior
The content field becomes the photo’s TikTok description. Limited to 2,200 characters and URLs are stripped if you include them — TikTok’s photo post API doesn’t honor inline URLs. Use the photoTitle field for the slideshow’s display title (90 chars max).
These do not work as media URLs:
- Google Drive — returns an HTML download page, not the file
- Dropbox — returns an HTML preview page
- OneDrive / SharePoint — returns HTML
- iCloud — returns HTML
Test your URL in an incognito browser window. If you see a webpage instead of the raw video or image, it will not work.
Media URLs must be:
- Publicly accessible (no authentication required).
- Returning actual media bytes with the correct
Content-Type header.
- Served from a domain TikTok permits — see TikTok’s verified-domain requirement below.
- Hosted on a fast CDN.
Large videos are auto-rejected during upload (5–10 MB per chunk). Photos must resolve to 1080 × 1920.
Verified domain requirement
TikTok requires the host of video_url / image_url to be on your TikTok-developer-portal verified-domain list. Add your CDN origin (or Postbreeze’s R2_PUBLIC_URL) to that list before scheduling — uploads from unverified hosts return url_ownership_unverified and Temporal won’t retry.
Analytics
Available metrics via the Analytics API:
| Metric | Available |
|---|
| Views | ✅ |
| Likes | ✅ |
| Comments | ✅ |
| Shares | ✅ |
| Followers (account-level) | ✅ |
| Watch time (seconds) | ✅ |
| Avg view duration (seconds) | ✅ |
| Profile visits | ❌ Not exposed by TikTok’s API |
| Reach | ❌ Not exposed by TikTok’s API |
Refresh cadence: every 12 hours. TikTok requires platform-side data older than 7 days to be re-fetched on demand, so historical analytics older than a week may show stale values until the next refresh tick.
Common errors
| Error | Meaning | Fix |
|---|
TT_PRIVACY_REQUIRED | privacy was not set on the platformOptions | Pass one of the four enum values. There is no default — TikTok requires explicit consent. |
TT_PRIVACY_NOT_ALLOWED | The creator’s account can’t post to the requested privacy (e.g. their account is private but you sent PUBLIC_TO_EVERYONE) | Pick a more restrictive value, or reconnect once the creator changes their default. |
TT_BRANDED_PRIVATE | brandContentToggle: true combined with non-public privacy | Branded content must be PUBLIC_TO_EVERYONE. |
TT_DOMAIN_UNVERIFIED | The media URL’s host isn’t on the verified-domain list | Add the CDN host to your TikTok developer portal. |
TT_RATE_LIMITED | You’ve hit TikTok’s per-creator publish rate limit | Wait — the publisher retries with backoff up to 15 minutes. |
TT_MEDIA_TOO_LARGE | Video > 500 MB or photo > 20 MB | Compress or transcode before uploading. |
TT_MIXED_MEDIA | Both photos and videos in mediaIds | TikTok publishes either a photo carousel or a single video; pick one. |
TT_DURATION_OUT_OF_RANGE | Video < 3s or > 10 min | Trim before uploading. |
What you can’t do
These features are not exposed by TikTok’s Content Posting API today:
- ❌ First comment (auto-post a follow-up reply)
- ❌ Add hashtags as separate metadata
- ❌ Add links (URLs in caption are stripped)
- ❌ Edit a post after publish
- ❌ Save as draft from API
- ❌ Schedule via TikTok’s native scheduler (we use our own queue)
- ❌ Post Stories or LIVE content
- ❌ Apply effects, filters, or sounds programmatically
Full control (nested shape)
The flat shape above covers every TikTok use case. If you’d rather group
every per-target field under targets[] — for example because you’re
fanning the same post out to multiple platforms and want each target’s
caption, media list, and options in one object — use the nested
shape. Replace content → caption, scheduledFor → scheduledAt,
platforms → targets, and accountId → socialAccountId. Everything
else is identical.
const post = await postbreeze.posts.create({
caption: "Behind the scenes from launch week ✨",
scheduledAt: new Date(Date.now() + 60_000).toISOString(),
mediaIds: ["med_video_…"],
targets: [
{
socialAccountId: "soc_tiktok_…",
platformOptions: {
platform: "TIKTOK_PERSONAL",
privacy: "PUBLIC_TO_EVERYONE",
allowComments: true,
allowDuet: false,
allowStitch: false,
autoAddMusic: false,
brandOrganicToggle: false,
brandContentToggle: false,
},
},
],
});