> ## Documentation Index
> Fetch the complete documentation index at: https://docs.postbreeze.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# TikTok

> Schedule and publish TikTok videos and photo carousels — privacy settings, branded content, and disclosures.

## 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

<Warning>
  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.
</Warning>

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](/concepts/platform-settings#tiktok-personal) and [Media uploads](/concepts/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](#full-control-nested-shape) when you'd
rather group every per-target field under `targets[]`.

<Warning>
  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.
</Warning>

<CodeGroup>
  ```js Node.js theme={null}
  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);
  ```

  ```python Python theme={null}
  import os
  from datetime import datetime, timedelta, timezone
  import requests

  API = "https://api.postbreeze.ai/api/v1"

  scheduled_for = (datetime.now(timezone.utc) + timedelta(seconds=60)).isoformat()
  res = requests.post(
      f"{API}/posts",
      headers={"Authorization": f"Bearer {os.environ['POSTBREEZE_API_KEY']}"},
      json={
          "content": "Behind the scenes from launch week ✨",
          "scheduledFor": scheduled_for,
          "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,
              },
          }],
      },
  )
  res.raise_for_status()
  post = res.json()
  print("Scheduled:", post)
  ```

  ```bash curl theme={null}
  curl -X POST https://api.postbreeze.ai/api/v1/posts \
    -H "Authorization: Bearer $POSTBREEZE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "Behind the scenes from launch week ✨",
      "scheduledFor": "2026-06-02T12:00:00Z",
      "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
        }
      }]
    }'
  ```
</CodeGroup>

## 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`.

<CodeGroup>
  ```js Node.js theme={null}
  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,
      },
    }],
  });
  ```

  ```python Python theme={null}
  res = requests.post(
      f"{API}/posts",
      headers={"Authorization": f"Bearer {os.environ['POSTBREEZE_API_KEY']}"},
      json={
          "content": "Quick tutorial #1",
          "scheduledFor": (datetime.now(timezone.utc) + timedelta(seconds=30)).isoformat(),
          "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,
              },
          }],
      },
  )
  res.raise_for_status()
  ```

  ```bash curl theme={null}
  curl -X POST https://api.postbreeze.ai/api/v1/posts \
    -H "Authorization: Bearer $POSTBREEZE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "Quick tutorial #1",
      "scheduledFor": "2026-06-02T12:00:30Z",
      "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
        }
      }]
    }'
  ```
</CodeGroup>

### 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.

<CodeGroup>
  ```js Node.js theme={null}
  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",
      },
    }],
  });
  ```

  ```python Python theme={null}
  res = requests.post(
      f"{API}/posts",
      headers={"Authorization": f"Bearer {os.environ['POSTBREEZE_API_KEY']}"},
      json={
          "content": "Trip recap — best moments from the weekend",
          "scheduledFor": (datetime.now(timezone.utc) + timedelta(seconds=30)).isoformat(),
          "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",
              },
          }],
      },
  )
  res.raise_for_status()
  ```

  ```bash curl theme={null}
  curl -X POST https://api.postbreeze.ai/api/v1/posts \
    -H "Authorization: Bearer $POSTBREEZE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "Trip recap — best moments from the weekend",
      "scheduledFor": "2026-06-02T12:00:30Z",
      "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"
        }
      }]
    }'
  ```
</CodeGroup>

### 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.

```js Node.js theme={null}
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
    },
  }],
});
```

## Media requirements

### 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](/concepts/media-uploads) for the presign + upload flow that produces `mediaIds`.

## Platform-specific fields

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).

## Media URL requirements

<Warning>
  **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.
</Warning>

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](/api-reference/overview#analytics):

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

```js Node.js theme={null}
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,
      },
    },
  ],
});
```
