---
title: "TikTok Content Posting API: the audit, direct post vs inbox, privacy levels"
description: "Post videos and photos with TikTok's Content Posting API: what an unaudited app can do, creator info, privacy levels, status checks, caps and errors."
url: https://rizamo.com/guides/tiktok-content-posting-api/
---

# TikTok Content Posting API: the audit, direct post vs inbox, privacy levels

TikTok's Content Posting API lets an app publish videos and photo posts to a creator's account, or drop them in the creator's TikTok inbox to finish in the app. The calls are short. What takes the time is everything around them: an audit before anything can be public, a creator info call before every post, a posting screen TikTok specifies down to the checkboxes, and a daily cap shared with every other app the creator uses.

This guide covers those rules, with each one linked to TikTok's own docs, and the calls in curl.

## The short version

- Two ways to post: **Direct Post** (`video.publish`) publishes to the profile; **Upload** (`video.upload`) sends a draft to the creator's inbox ([get started](https://developers.tiktok.com/doc/content-posting-api-get-started)).
- Until your app passes TikTok's audit, every post is private (`SELF_ONLY`), the accounts must be private, and at most 5 people can post through the app in 24 hours ([content sharing guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines)).
- Before each post, call creator info and use only the privacy levels it returns ([query creator info](https://developers.tiktok.com/doc/content-posting-api-reference-query-creator-info)).
- TikTok fetches a file from your URL only if you verified the domain or URL prefix; a file on the person's device goes up in chunks instead ([media transfer guide](https://developers.tiktok.com/doc/content-posting-api-media-transfer-guide)).
- A post is done when the status call says `PUBLISH_COMPLETE`, not when the first call returns ([get post status](https://developers.tiktok.com/doc/content-posting-api-reference-get-video-status)).
- Around 15 direct posts a day per creator, shared across every app that posts for them ([content sharing guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines)).

## Direct Post or Upload

| | Direct Post | Upload (inbox draft) |
| --- | --- | --- |
| Scope | `video.publish` | `video.upload` |
| Video endpoint | `/v2/post/publish/video/init/` | `/v2/post/publish/inbox/video/init/` |
| Caption, privacy, interactions | Your app sends them | The creator sets them in TikTok |
| What the creator does | Nothing after your app's screen | Opens the inbox notification and posts from TikTok's editor |
| Limit | About 15 posts a day per creator | At most 5 pending drafts in 24 hours |

Sources: [direct post](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post), [upload](https://developers.tiktok.com/doc/content-posting-api-reference-upload-video), [content sharing guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines).

Upload is the easier start: the creator finishes the post in TikTok, so your app sends only the file. TikTok asks you to tell people they must tap the inbox notification to finish the post ([upload](https://developers.tiktok.com/doc/content-posting-api-reference-upload-video)). Direct Post is what you want for scheduling or for an agent that publishes, and it is the one the rules below are mostly about.

Photo posts use one endpoint for both, `/v2/post/publish/content/init/`, with `post_mode` set to `DIRECT_POST` or `MEDIA_UPLOAD` ([photo post](https://developers.tiktok.com/doc/content-posting-api-reference-photo-post)).

## The audit: what an unaudited app can do

Getting the `video.publish` scope approved does not make posts public. Until the app passes a separate audit, TikTok's guidelines set these limits ([content sharing guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines)):

- Every post is `SELF_ONLY`: only the creator sees it.
- Every account posting through the app must be set to private at the time of posting. A public account gets `unaudited_client_can_only_post_to_private_accounts` (HTTP 403, [direct post](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post)).
- At most 5 people can post through the app in a 24-hour window.
- To make a post public later, the creator switches the account to public, then each post's privacy to "Everyone".

You apply for the audit from the [Content Posting API page](https://developers.tiktok.com/application/content-posting-api) in the developer portal. The audit form also asks for your usage estimates, which set your app's daily cap on active creators ([content sharing guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines)). The guidelines rule out apps built only for testing, so build the posting screen described below before you apply: it is what the reviewers look at.

## Scopes and tokens

The person logs in with TikTok's Login Kit, and your server exchanges the code at `https://open.tiktokapis.com/v2/oauth/token/` ([get a user access token](https://developers.tiktok.com/doc/oauth-get-a-user-access-token)).

- The access token lasts 2 hours and the refresh token 180 days ([manage access tokens](https://developers.tiktok.com/doc/oauth-user-access-token-management)). TikTok says to schedule refreshes from `expires_in` and `refresh_expires_in` rather than hard-code these numbers.
- Each refresh returns a new refresh token and invalidates the one you sent. Store the new one every time: if a replaced token is used again, the person has to authorize your app again (same page).
- Two refreshes of the same login at the same time can lose it, since the second sends a token the first just replaced. Let one process own each login's refresh.

## Step 1: Query creator info

TikTok requires this call each time you show the posting screen: it says who is posting, which privacy levels the account allows, and what the creator turned off ([query creator info](https://developers.tiktok.com/doc/content-posting-api-reference-query-creator-info)).

```bash title="Query creator info"
export TT_TOKEN="<USER_ACCESS_TOKEN>"

curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/creator_info/query/" \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json; charset=UTF-8"
```

```json title="Response"
{
  "data": {
    "creator_avatar_url": "https://...",
    "creator_username": "autumnroasters",
    "creator_nickname": "Autumn Roasters",
    "privacy_level_options": ["FOLLOWER_OF_CREATOR", "MUTUAL_FOLLOW_FRIENDS", "SELF_ONLY"],
    "comment_disabled": false,
    "duet_disabled": true,
    "stitch_disabled": true,
    "max_video_post_duration_sec": 180
  },
  "error": { "code": "ok", "message": "", "log_id": "..." }
}
```

What to do with each field (same page and the [guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines)):

- `creator_nickname`: show it on the posting screen, so the person sees which account will post.
- `privacy_level_options`: the only values you may send. A public account gets `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS` and `SELF_ONLY`; a private one gets `FOLLOWER_OF_CREATOR` instead of `PUBLIC_TO_EVERYONE`. Anything else fails with `privacy_level_option_mismatch`.
- `comment_disabled`, `duet_disabled`, `stitch_disabled`: when true, grey out that option. Duet and Stitch are always off on private accounts.
- `max_video_post_duration_sec`: check the video's length against it before you send anything.
- An error such as `spam_risk_too_many_posts` or `reached_active_user_cap` here means stop: the guidelines say to end the attempt when the creator cannot post right now. Creator info returns these with HTTP 200, so check `error.code`, not the HTTP status.

## Step 2: Start the post

With a URL, TikTok downloads the file itself. This needs the URL's domain or prefix verified in your app's settings in the developer portal; otherwise the call fails with `url_ownership_unverified` ([media transfer guide](https://developers.tiktok.com/doc/content-posting-api-media-transfer-guide)). The URL must be https, must not redirect, and must stay reachable until the download ends, which times out after an hour.

```bash title="Direct post a video from a URL"
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/video/init/" \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json; charset=UTF-8" \
  -d '{
    "post_info": {
      "title": "First roast of the season #coffee",
      "privacy_level": "SELF_ONLY",
      "disable_comment": false,
      "disable_duet": true,
      "disable_stitch": true,
      "video_cover_timestamp_ms": 1000,
      "brand_content_toggle": false,
      "brand_organic_toggle": false,
      "is_aigc": false
    },
    "source_info": {
      "source": "PULL_FROM_URL",
      "video_url": "https://media.example.com/videos/first-roast.mp4"
    }
  }'
# {"data":{"publish_id":"v_pub_url~v2.123456789"},"error":{"code":"ok",...}}
```

The `post_info` fields ([direct post](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post)):

- `privacy_level`: required, one of the creator info options. `SELF_ONLY` is the only one that works before the audit.
- `title`: the caption, up to 2,200 UTF-16 code units, with hashtags and mentions.
- `disable_comment`, `disable_duet`, `disable_stitch`: what the person chose on your screen.
- `video_cover_timestamp_ms`: the cover frame; the first frame if unset.
- `brand_content_toggle`: a paid partnership for a third party. `brand_organic_toggle`: promoting the creator's own business. Send both, from the person's answer.
- `is_aigc`: adds TikTok's AI-generated label.

Keep the `publish_id`: it is how you follow the post.

## Upload a file instead

If the file is on the person's device, use `FILE_UPLOAD`; TikTok's guidelines say files already on your servers must use `PULL_FROM_URL`. The first call says how the file is split; TikTok answers with an `upload_url`, valid for an hour, and you send the bytes to it with `PUT` ([media transfer guide](https://developers.tiktok.com/doc/content-posting-api-media-transfer-guide), [direct post](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post)).

```bash title="1. Start a file upload (12 MB, one chunk)"
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/video/init/" \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json; charset=UTF-8" \
  -d '{
    "post_info": { "title": "First roast of the season #coffee", "privacy_level": "SELF_ONLY",
      "disable_comment": false, "disable_duet": true, "disable_stitch": true, "brand_content_toggle": false },
    "source_info": { "source": "FILE_UPLOAD", "video_size": 12582912,
      "chunk_size": 12582912, "total_chunk_count": 1 }
  }'
# {"data":{"publish_id":"v_pub_file~v2-1.123456789","upload_url":"https://open-upload.tiktokapis.com/video/?upload_id=...&upload_token=..."},...}
```

```bash title="2. Send the bytes"
curl -s -X PUT "<UPLOAD_URL_WITH_ITS_QUERY_STRING>" \
  -H "Content-Range: bytes 0-12582911/12582912" \
  -H "Content-Length: 12582912" \
  -H "Content-Type: video/mp4" \
  --data-binary "@first-roast.mp4"
```

The chunk rules trip people up, so here they are in full (same page):

- A file under 5 MB goes in one piece, with `chunk_size` equal to the file size.
- Otherwise each chunk is 5 to 64 MB, sent in order, at most 1,000 of them. A file over 64 MB must be split.
- `total_chunk_count` is `video_size` divided by `chunk_size`, rounded down. The leftover bytes ride on the last chunk, which can be up to 128 MB.
- Each `PUT` answers 206 while chunks are missing and 201 when the file is complete. A 403 means the `upload_url` expired: start again.

## Step 3: Check the status

The init call only starts the work. Ask for the status with the `publish_id` until it settles, or subscribe to the `post.publish.*` webhooks ([get post status](https://developers.tiktok.com/doc/content-posting-api-reference-get-video-status)).

```bash title="Fetch the status"
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/status/fetch/" \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json; charset=UTF-8" \
  -d '{ "publish_id": "v_pub_url~v2.123456789" }'
```

| `status` | Meaning |
| --- | --- |
| `PROCESSING_DOWNLOAD` | TikTok is pulling your URL |
| `PROCESSING_UPLOAD` | Your chunks are arriving |
| `SEND_TO_USER_INBOX` | The draft is in the creator's inbox (Upload only) |
| `PUBLISH_COMPLETE` | Posted |
| `FAILED` | Read `fail_reason` |

- `publicaly_available_post_id` (TikTok's spelling) stays empty until the post is public and has passed moderation. Moderation usually takes under a minute but can take a few hours. A private post never gets one, since the ID is only returned for public posts (same page).
- The status endpoint allows 30 requests a minute per token. Poll every few seconds, back off, and stop at a time limit you choose.
- Tell the person it may take a few minutes for the post to appear: the guidelines ask for that notice.

## Photo posts

Photos go to `/v2/post/publish/content/init/` and must come from URLs: there is no file upload for photos ([photo post](https://developers.tiktok.com/doc/content-posting-api-reference-photo-post)). Up to 35 images (same page), JPEG or WebP, at most 20 MB and 1080p each ([media transfer guide](https://developers.tiktok.com/doc/content-posting-api-media-transfer-guide)).

```bash title="Direct post three photos"
curl -s -X POST "https://open.tiktokapis.com/v2/post/publish/content/init/" \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json; charset=UTF-8" \
  -d '{
    "media_type": "PHOTO",
    "post_mode": "DIRECT_POST",
    "post_info": {
      "title": "Autumn menu",
      "description": "Three new drinks this week #coffee",
      "privacy_level": "SELF_ONLY",
      "disable_comment": false,
      "auto_add_music": true,
      "brand_content_toggle": false,
      "brand_organic_toggle": false
    },
    "source_info": {
      "source": "PULL_FROM_URL",
      "photo_cover_index": 0,
      "photo_images": [
        "https://media.example.com/photos/menu-1.webp",
        "https://media.example.com/photos/menu-2.webp",
        "https://media.example.com/photos/menu-3.webp"
      ]
    }
  }'
# {"data":{"publish_id":"p_pub_url~v2.123456789"},...}
```

Photos differ from video in a few fields: `title` takes 90 UTF-16 code units and `description` 4,000; only comments can be turned off (no Duet or Stitch); `auto_add_music` adds music on a direct post; `photo_cover_index` picks the cover, counting from 0 (same page).

## The posting screen TikTok requires

This is where most audits are won or lost. Before your app sends anything, the person must see a screen with ([content sharing guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines)):

- The creator's nickname, from creator info.
- A preview of the content.
- A title they can edit, including any text or hashtags you filled in.
- Privacy as a dropdown of the creator info options, with **no default**.
- Allow Comment, Duet and Stitch (photos: Comment only), all unchecked to start, greyed out where creator info says disabled.
- A commercial content disclosure toggle, off by default, with "Your brand" and "Branded content". If it is on with neither picked, publishing is disabled. Branded content cannot be private.
- The declaration before the publish button: "By posting, you agree to TikTok's Music Usage Confirmation", plus the Branded Content Policy when branded content is picked.

And no watermark, logo or promotional text added to the person's content. Nothing is sent to TikTok until the person expressly agrees (same page).

**For an agent this means one thing: it cannot decide for the person.** An agent can draft the caption and pick the media, but privacy, the interaction settings and the disclosure have no defaults, so it has to ask, show the creator's nickname, and get a clear yes before it calls the init endpoint. A tool that takes these as required arguments and refuses a missing one keeps the agent honest.

## Limits

| Limit | Value | Source |
| --- | --- | --- |
| Direct posts per creator | About 15 a day, shared with every app using Direct Post | [Guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines) |
| Pending inbox drafts | 5 in 24 hours | [Upload](https://developers.tiktok.com/doc/content-posting-api-reference-upload-video) |
| Creators per app | Set from your audit form; 5 in 24 hours before the audit | [Guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines) |
| Init calls (video, upload, photo) | 6 a minute per token | [Direct post](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post) |
| Creator info | 20 a minute per token | [Query creator info](https://developers.tiktok.com/doc/content-posting-api-reference-query-creator-info) |
| Status | 30 a minute per token | [Get post status](https://developers.tiktok.com/doc/content-posting-api-reference-get-video-status) |

TikTok does not publish the exact daily cap: the guidelines say "typically around 15". Since other apps the creator uses count toward it, your own count can be under 15 and still hit `spam_risk_too_many_posts`. Treat that error as "try later or post from the app", never as a reason to retry.

## Media requirements

From the [media transfer guide](https://developers.tiktok.com/doc/content-posting-api-media-transfer-guide):

| | Video | Photo |
| --- | --- | --- |
| Formats | MP4 (recommended), MOV, WebM | JPEG, WebP |
| Codecs | H.264 (recommended), H.265, VP8, VP9 | |
| Size | Up to 4 GB | Up to 20 MB each |
| Dimensions | 360 to 4096 px on each side | Up to 1080p |
| Frame rate | 23 to 60 fps | |
| Length | Direct Post: up to the creator's `max_video_post_duration_sec`; Upload: up to 10 minutes | Up to 35 images ([photo post](https://developers.tiktok.com/doc/content-posting-api-reference-photo-post)) |

All creators can post 3-minute videos and some can post 5 or 10, which is why the duration comes from creator info rather than a constant (same page).

## Common errors

| Error | What it means | What to do |
| --- | --- | --- |
| `unaudited_client_can_only_post_to_private_accounts` | Unaudited app, public account | Set the account to private, or pass the audit |
| `privacy_level_option_mismatch` | A privacy value creator info did not offer | Use only `privacy_level_options` |
| `url_ownership_unverified` | The video or photo URL is not on a verified domain or prefix | Verify the domain or URL prefix in the developer portal |
| `spam_risk_too_many_posts` | The creator hit the daily cap | Stop; the person can post from the TikTok app |
| `spam_risk_too_many_pending_share` | 5 inbox drafts pending | Wait for the creator to post or discard them |
| `reached_active_user_cap` | Your app hit its daily creator cap | Wait, or ask for a higher cap |
| `rate_limit_exceeded` (429) | Over the per-minute limit | Back off |
| `fail_reason: video_pull_failed` / `photo_pull_failed` | TikTok could not download the file | Check the URL is public, https, with no redirect |
| `fail_reason: duration_check_failed`, `frame_rate_check_failed`, `picture_size_check_failed`, `file_format_check_failed` | The file broke a media rule | Re-encode it |

Sources: [direct post](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post), [upload](https://developers.tiktok.com/doc/content-posting-api-reference-upload-video), [query creator info](https://developers.tiktok.com/doc/content-posting-api-reference-query-creator-info), [get post status](https://developers.tiktok.com/doc/content-posting-api-reference-get-video-status). `spam_risk_user_banned_from_posting`, `spam_risk_text`, `spam_risk` and `auth_removed` are final: do not retry them (get post status).

## Where Rizamo fits

If you post to a handful of your own accounts, Upload mode or an unaudited app with private accounts may be all you need, and the calls above are the whole job. For an app that serves other people, the audit, the posting screen and the shared cap are the work. Rizamo is a social media API and MCP server built to take that layer off your hands: it reads creator info fresh for each post, asks for every choice TikTok requires with no defaults, follows each post to its final status, and counts each account's daily posts before sending.

Rizamo is opening soon: [join the waitlist](/#waitlist). What it will support on TikTok: [TikTok on Rizamo](/channels/tiktok/).
