# Publish to TikTok

> Publish a TikTok video, photo, or carousel.

Canonical: <https://www.tryadeli.com/docs/api/posts/publish-tiktok>

`POST https://app.tryadeli.com/api/v1/posts`

Authorization: `Bearer <API key>` — see [Authentication](https://www.tryadeli.com/docs/authentication)

TikTok publishing is asynchronous. A successful request means TikTok accepted
the content, not that it is live. Adeli publishes through TikTok API for
Business, which pulls every video and photo from a URL: whatever you send —
base64, multipart, or a URL — Adeli stages it on its own verified media host
first.

> **Note: Videos are public or drafts**
>
> TikTok for Business has no privacy setting for videos. A `DIRECT_POST` video
> is published to everyone, so its `privacy_level` must be
> `PUBLIC_TO_EVERYONE` or left out; anything else is
> `422 privacy_level_unsupported` rather than a post that is more public than
> you asked for. To keep a video private, send `post_mode: "MEDIA_UPLOAD"`: it
> arrives as a draft in the creator's TikTok inbox. Photo posts accept every
> `privacy_level` the creator currently allows.

TikTok allows 15 API posts per account per day.

Before a Direct Post, [check creator capabilities](https://www.tryadeli.com/docs/api/posts/get-tiktok-creator-info)
first.

**Body**

| Name                    | Type                              | Required | Description                                                                                                                                                                                                     |
| ----------------------- | --------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `platform`              | `"tiktok"`                        | Yes      |                                                                                                                                                                                                                 |
| `accountId`             | `uuid`                            | Yes      | A connected TikTok account.                                                                                                                                                                                     |
| `profileId`             | `uuid`                            |          | Optional, because accountId names its profile. If you pass both, they must agree.                                                                                                                               |
| `post_mode`             | `"DIRECT_POST" \| "MEDIA_UPLOAD"` |          | Defaults to `DIRECT_POST`, which publishes. `MEDIA_UPLOAD` sends a draft to the creator's TikTok inbox to finish in the app; a video draft carries the media only, a photo draft its title and description too. |
| `tiktok_title`          | `string`                          |          | Up to 2,200 characters for a video, and up to 90 for photos.                                                                                                                                                    |
| `tiktok_description`    | `string`                          |          | Up to 4,000 characters. Photo posts only.                                                                                                                                                                       |
| `privacy_level`         | `string`                          |          | Videos: `PUBLIC_TO_EVERYONE` or omitted. Photos: one of `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY` that the creator currently allows; defaults to `PUBLIC_TO_EVERYONE`.  |
| `music_usage_confirmed` | `true`                            | Yes      | Must be literally true. You are confirming the creator agreed to TikTok's music usage terms.                                                                                                                    |
| `disable_comment`       | `boolean`                         |          | Defaults to false.                                                                                                                                                                                              |
| `disable_duet`          | `boolean`                         |          | Video only. Defaults to false.                                                                                                                                                                                  |
| `disable_stitch`        | `boolean`                         |          | Video only. Defaults to false.                                                                                                                                                                                  |
| `cover_timestamp`       | `integer`                         |          | Video only. Milliseconds into the video to use as the cover. Defaults to 1000.                                                                                                                                  |
| `photo_cover_index`     | `integer`                         |          | Photos only. Defaults to 0, and must be less than the number of photos.                                                                                                                                         |
| `auto_add_music`        | `boolean`                         |          | Photos only. Defaults to false.                                                                                                                                                                                 |
| `is_aigc`               | `boolean`                         |          | Marks the content as AI-generated. Defaults to false.                                                                                                                                                           |
| `brand_content_toggle`  | `boolean`                         |          | Paid partnership. Requires `privacy_level: \"PUBLIC_TO_EVERYONE\"` on a Direct Post.                                                                                                                            |
| `brand_organic_toggle`  | `boolean`                         |          | Promotes the creator's own brand.                                                                                                                                                                               |
| `video`                 | `object`                          |          | Either `{ content_type: "video/mp4", base64 }` or `{ url }` pointing at public HTTPS. Mutually exclusive with `photos`.                                                                                         |
| `photos`                | `object[]`                        |          | 1 to 35 photos, each `{ content_type, base64 }`. Remote photo URLs are not accepted. Mutually exclusive with `video`.                                                                                           |

> **Note: Exactly one of video or photos**
>
> Sending both, or neither, is `400 invalid_request`.

The request example sends a video from a public URL to the creator's inbox.
A video published publicly:

```json
{
  "platform": "tiktok",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "tiktok_title": "Morning chorus #birds",
  "privacy_level": "PUBLIC_TO_EVERYONE",
  "disable_duet": false,
  "disable_stitch": true,
  "music_usage_confirmed": true,
  "video": { "url": "https://media.example/video.mp4" }
}
```

A photo carousel posted directly, visible only to the creator:

```json
{
  "platform": "tiktok",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "post_mode": "DIRECT_POST",
  "tiktok_title": "Backyard birds",
  "tiktok_description": "Cardinal, Titmouse, and Robin",
  "privacy_level": "SELF_ONLY",
  "photo_cover_index": 0,
  "auto_add_music": false,
  "music_usage_confirmed": true,
  "photos": [
    { "content_type": "image/webp", "base64": "..." },
    { "content_type": "image/jpeg", "base64": "..." }
  ]
}
```

## Multipart uploads

For large videos, send `multipart/form-data` instead. Scalar fields use the same
names, booleans must be the strings `"true"` or `"false"`, and the media is
either one `video` file or 1–35 repeated `photos[]` files. Multipart is TikTok
only.

```bash
curl --fail-with-body -X POST "https://app.tryadeli.com/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -F 'platform=tiktok' \
  -F "accountId=$ACCOUNT_ID" \
  -F 'post_mode=DIRECT_POST' \
  -F 'privacy_level=PUBLIC_TO_EVERYONE' \
  -F 'tiktok_title=Morning chorus' \
  -F 'music_usage_confirmed=true' \
  -F 'video=@video.mp4;type=video/mp4'
```

```bash
curl --fail-with-body -X POST "https://app.tryadeli.com/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -F 'platform=tiktok' \
  -F "accountId=$ACCOUNT_ID" \
  -F 'post_mode=MEDIA_UPLOAD' \
  -F 'music_usage_confirmed=true' \
  -F 'photos[]=@cardinal.webp;type=image/webp' \
  -F 'photos[]=@robin.jpg;type=image/jpeg'
```

## The response

Success returns `202` with a `publishId`. Pass it to
[TikTok publish status](https://www.tryadeli.com/docs/api/posts/get-tiktok-publish-status) to learn
whether the post went live.

> **Note: statusUrl is a relative path**
>
> Join it to your base URL before requesting it. `warnings` is present only when
> non-empty — inbox uploads report the settings TikTok ignored, since a video
> sent to the inbox carries media and nothing else.

## Request examples

**cURL**

```bash
curl --fail-with-body -X POST "https://app.tryadeli.com/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "platform": "tiktok",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "post_mode": "MEDIA_UPLOAD",
  "tiktok_title": "Bird video",
  "music_usage_confirmed": true,
  "video": {
    "url": "https://media.example/video.mp4"
  }
}'
```

**JavaScript**

```javascript
const response = await fetch("https://app.tryadeli.com/api/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ADELI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    platform: "tiktok",
    accountId: "00000000-0000-0000-0000-000000000000",
    post_mode: "MEDIA_UPLOAD",
    tiktok_title: "Bird video",
    music_usage_confirmed: true,
    video: {
      url: "https://media.example/video.mp4",
    },
  }),
});
console.log(await response.json());
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://app.tryadeli.com/api/v1/posts",
    headers={"Authorization": f"Bearer {os.environ['ADELI_API_KEY']}"},
    json={
        "platform": "tiktok",
        "accountId": "00000000-0000-0000-0000-000000000000",
        "post_mode": "MEDIA_UPLOAD",
        "tiktok_title": "Bird video",
        "music_usage_confirmed": True,
        "video": {
            "url": "https://media.example/video.mp4",
        },
    },
)
print(response.json())
```

## Responses

### 202

```json
{
  "platform": "tiktok",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "profileId": "00000000-0000-4000-8000-000000000001",
  "publishId": "v_pub_url~v1.2345123456789123456",
  "status": "ACCEPTED",
  "statusUrl": "/api/v1/posts/tiktok/status?accountId=...&profileId=...&publishId=...",
  "warnings": [
    { "code": "video_draft_media_only", "message": "TikTok video drafts receive media only; post settings must be completed in TikTok" }
  ]
}
```

### 422

```json
{
  "error": {
    "code": "privacy_level_unsupported",
    "message": "TikTok videos publish publicly; use PUBLIC_TO_EVERYONE, or post_mode MEDIA_UPLOAD to send a draft"
  }
}
```
