Posts
Publish TikTok videos and photo carousels with privacy controls, AI disclosure, and scheduling.
Publish to TikTok through the shared posts surface via Direct Post. Every TikTok destination must set privacyLevel and use media hosted on Onepostly's CDN.
Quick Start
Publish a video in under a minute:
import { Configuration, PostsApi } from "@onepostly/sdk";
const postsApi = new PostsApi(
new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);
const response = await postsApi.createPost({
createPostBody: {
"text": "Ship day, the video cut",
"mediaUrls": [
"https://cdn.onepostly.com/media/demo.mp4"
],
"mediaKind": "video",
"destinations": [
{
"accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
"privacyLevel": "PUBLIC_TO_EVERYONE"
}
]
},
});
console.log(response);{
"post": {
"id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
"text": "Ship day, the video cut",
"mediaUrls": [
"https://cdn.onepostly.com/media/demo.mp4"
],
"mediaKind": "video",
"status": "queued",
"scheduledFor": null,
"timezone": null,
"destinations": [
{
"id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
"accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
"platform": "tiktok",
"status": "draft",
"externalPostId": "17895695668004550",
"externalUrl": "https://www.tiktok.com/@you/video/17895695668004550",
"errorCode": null,
"errorMessage": null,
"publishedAt": "2026-08-25T10:00:00.000Z",
"metrics": null
}
],
"createdAt": "2026-08-25T10:00:00.000Z",
"updatedAt": "2026-08-25T10:00:00.000Z"
}
}The response is 202 Accepted with status: "queued". Poll GET /v1/posts/:id until the destination is published or failed, or receive webhooks instead of polling. When published, public videos carry the TikTok post_id. Private posts carry a publish_id. A destination can reach published before its post link exists; externalUrl is filled in once TikTok makes the post publicly viewable.
Content Types
TikTok destinations accept two content types. You cannot mix photos and videos in the same post.
mediaKind | Content | Media count | Formats |
|---|---|---|---|
video | Single video | Exactly 1 URL | MP4, MOV, WebM |
multi-image | Photo carousel | 2 to 35 URLs | JPEG, WebP |
image | Single photo | 1 URL | JPEG, WebP |
text / stories | No | No | Not supported on TikTok |
Video Post
9:16 vertical (1080 × 1920, H.264, 30 fps) works well on TikTok. Duets and stitches are enabled by default; pass true to disable either one:
import { Configuration, PostsApi } from "@onepostly/sdk";
const postsApi = new PostsApi(
new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);
const response = await postsApi.createPost({
createPostBody: {
"text": "New cooking tutorial #recipe #foodtok",
"mediaUrls": [
"https://cdn.onepostly.com/media/cooking.mp4"
],
"mediaKind": "video",
"destinations": [
{
"accountId": "YOUR_TIKTOK_ACCOUNT_ID",
"privacyLevel": "PUBLIC_TO_EVERYONE"
}
]
},
});
console.log(response);{
"post": {
"id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
"text": "New cooking tutorial #recipe #foodtok",
"mediaUrls": [
"https://cdn.onepostly.com/media/cooking.mp4"
],
"mediaKind": "video",
"status": "queued",
"scheduledFor": null,
"timezone": null,
"destinations": [
{
"id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
"accountId": "YOUR_TIKTOK_ACCOUNT_ID",
"platform": "tiktok",
"status": "draft",
"externalPostId": "17895695668004550",
"externalUrl": "https://www.tiktok.com/@you/video/17895695668004550",
"errorCode": null,
"errorMessage": null,
"publishedAt": "2026-08-25T10:00:00.000Z",
"metrics": null
}
],
"createdAt": "2026-08-25T10:00:00.000Z",
"updatedAt": "2026-08-25T10:00:00.000Z"
}
}Photo Carousel
Up to 35 images. text becomes the photo title (90 characters, auto-truncated). Use description for a full caption up to 4,000 characters. It falls back to text when omitted:
import { Configuration, PostsApi } from "@onepostly/sdk";
const postsApi = new PostsApi(
new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);
const response = await postsApi.createPost({
createPostBody: {
"text": "My travel highlights",
"mediaUrls": [
"https://cdn.onepostly.com/media/photo1.jpg",
"https://cdn.onepostly.com/media/photo2.jpg",
"https://cdn.onepostly.com/media/photo3.jpg"
],
"mediaKind": "multi-image",
"destinations": [
{
"accountId": "YOUR_TIKTOK_ACCOUNT_ID",
"privacyLevel": "PUBLIC_TO_EVERYONE",
"description": "Full trip recap from our weekend adventure across the coast. #travel #roadtrip"
}
]
},
});
console.log(response);{
"post": {
"id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
"text": "My travel highlights",
"mediaUrls": [
"https://cdn.onepostly.com/media/photo1.jpg",
"https://cdn.onepostly.com/media/photo2.jpg",
"https://cdn.onepostly.com/media/photo3.jpg"
],
"mediaKind": "multi-image",
"status": "queued",
"scheduledFor": null,
"timezone": null,
"destinations": [
{
"id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
"accountId": "YOUR_TIKTOK_ACCOUNT_ID",
"platform": "tiktok",
"status": "draft",
"externalPostId": "17895695668004550",
"externalUrl": "https://www.tiktok.com/@you/video/17895695668004550",
"errorCode": null,
"errorMessage": null,
"publishedAt": "2026-08-25T10:00:00.000Z",
"metrics": null
}
],
"createdAt": "2026-08-25T10:00:00.000Z",
"updatedAt": "2026-08-25T10:00:00.000Z"
}
}Single Photo
mediaKind: "image" publishes one photo with the same title and description rules as a carousel:
import { Configuration, PostsApi } from "@onepostly/sdk";
const postsApi = new PostsApi(
new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);
const response = await postsApi.createPost({
createPostBody: {
"text": "Sunset from the studio",
"mediaUrls": [
"https://cdn.onepostly.com/media/sunset.jpg"
],
"mediaKind": "image",
"destinations": [
{
"accountId": "YOUR_TIKTOK_ACCOUNT_ID",
"privacyLevel": "PUBLIC_TO_EVERYONE",
"description": "Shot on our last studio day. #travel"
}
]
},
});
console.log(response);{
"post": {
"id": "b3e1a7c2-8f4d-4e6a-9c1b-2d3e4f5a6b7c",
"text": "Sunset from the studio",
"mediaUrls": [
"https://cdn.onepostly.com/media/sunset.jpg"
],
"mediaKind": "image",
"status": "queued",
"scheduledFor": null,
"timezone": null,
"destinations": [
{
"id": "d4e5f6a7-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
"accountId": "YOUR_TIKTOK_ACCOUNT_ID",
"platform": "tiktok",
"status": "draft",
"externalPostId": "17895695668004550",
"externalUrl": "https://www.tiktok.com/@you/video/17895695668004550",
"errorCode": null,
"errorMessage": null,
"publishedAt": "2026-08-25T10:00:00.000Z",
"metrics": null
}
],
"createdAt": "2026-08-25T10:00:00.000Z",
"updatedAt": "2026-08-25T10:00:00.000Z"
}
}Media Requirements
Videos
| Property | Requirement |
|---|---|
| Max videos | 1 per post |
| Formats | MP4, MOV, WebM |
| Max file size | 4 GB |
| Max duration | Account-specific, up to 10 minutes |
| Recommended | 9:16 vertical, 1080 × 1920, H.264, 30 fps |
The exact duration a creator may upload is returned by creator info as maxVideoPostDurationSec.
Photos
| Property | Requirement |
|---|---|
| Max photos | 35 per carousel (1 for a single photo) |
| Formats | JPEG, WebP |
| Recommended | 1080 × 1920 (9:16) |
Media Hosting
TikTok rejects media hosted anywhere else. Google Drive, Dropbox, OneDrive,
iCloud, and your own domain all fail, because TikTok's PULL_FROM_URL only
accepts hosts verified in the developer app. Onepostly's CDN is the verified
host.
The flow is always the same:
- Upload the file through the Media API
- Pass the returned
cdn.onepostly.comURL inmediaUrls - TikTok fetches the asset directly at publish time
Destination Parameters
Every TikTok destination accepts the following parameters. All of them except privacyLevel are optional.
Prop
Type
Privacy Levels
Every TikTok destination must set privacyLevel:
| Value | Meaning |
|---|---|
PUBLIC_TO_EVERYONE | Everyone |
MUTUAL_FOLLOW_FRIENDS | Friends only |
FOLLOWER_OF_CREATOR | Followers only |
SELF_ONLY | Only the creator |
Which values a creator may use depends on their account settings. A private account, for example, cannot post PUBLIC_TO_EVERYONE. Values outside the creator's allowed set are rejected with INVALID_PRIVACY_LEVEL.
Creator Info
Before posting, check which privacy levels and interactions the creator allows:
import { Configuration, ConnectionsApi } from "@onepostly/sdk";
const connectionsApi = new ConnectionsApi(
new Configuration({ apiKey: process.env.ONEPOSTLY_API_KEY })
);
const response = await connectionsApi.getTikTokCreatorInfo({
id: "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
});
console.log(response);{
"accountId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
"platform": "tiktok",
"creatorInfo": {
"creatorAvatarUrl": "https://cdn.onepostly.com/avatars/onepostly.jpg",
"creatorUsername": "onepostly",
"creatorNickname": "Onepostly",
"privacyLevelOptions": [
"PUBLIC_TO_EVERYONE"
],
"commentDisabled": false,
"duetDisabled": false,
"stitchDisabled": false,
"maxVideoPostDurationSec": 600
},
"fetchedAt": "2026-08-25T10:00:00.000Z"
}Prop
Type
Schedule
Add scheduledFor (local wall time) and timezone to the same call. The request shape is otherwise identical to an immediate post.
timezonedefaults toUTC. Use an IANA name likeEurope/Istanbul- Do not append
Zor an offset toscheduledFor. It is local wall time - Scheduled posts return
201 Createdand publish automatically at the scheduled moment - Max 1 year ahead
- Cancel with
posts.cancelPost({ id })/posts.cancel_post(id=…), orDELETE /v1/posts/:id, any time before then
Direct Posting at Capacity
A TikTok post can fail with CAPACITY_LIMITED even though nothing is wrong with the account or the media. TikTok caps how many distinct accounts can direct-post through one developer app in a rolling 24-hour window (TikTok's own error for it is reached_active_user_cap). Every Onepostly customer publishes through the same TikTok app, so the budget is app-wide: it is not tied to your account, your API key, or how many posts you have made. An account that has already direct-posted inside the window keeps posting; the cap only blocks accounts that would need a new slot.
The window is rolling, so slots free up continuously as activity from 24 hours ago ages out. There is no midnight reset to wait for.
The post is marked failed and is not retried automatically. Retry it a few hours later, or reschedule it. The posting velocity limits (25 posts per hour, 50 per day for TikTok) apply on top, per account.
Publish Errors
| Code | Cause | Fix |
|---|---|---|
PRIVACY_LEVEL_REQUIRED | Destination has no privacyLevel | Set privacyLevel on every TikTok destination |
INVALID_PRIVACY_LEVEL | Value outside the creator's allowed set | Fetch creator info and use one of privacyLevelOptions |
INVALID_MEDIA | Unsupported format, size over 4 GB, or a URL not on Onepostly's CDN | Upload via the Media API and use its URLs |
MEDIA_UPLOAD_FAILED | TikTok rejected the upload | Check the error message for the TikTok reason |
PUBLISH_FAILED | Content moderation flagged the post, or duplicate content detected | Review the content, modify the caption or media, then retry |
SPAM_RISK | TikTok flagged this post as potentially risky | Review content. TikTok's API moderation is stricter than the native app |
POSTING_LIMIT_REACHED | The account has posted too many times in a short window | Wait for the window to pass, then post again |
ACCOUNT_RESTRICTED | TikTok restricted the account from posting | Resolve it in the TikTok app before posting again |
CAPACITY_LIMITED | TikTok's app-wide cap on distinct posting accounts per rolling 24h | Retry a few hours later or reschedule. See Direct posting at capacity |
TOKEN_INVALID | Access token expired or revoked | Reconnect the account |
PERMISSION_DENIED | Connection lacks the video.publish scope | Reconnect the account and grant publish permission |
UNSUPPORTED_MEDIA_KIND | mediaKind: "text" or "stories" | Use video, image, or multi-image |
UNSUPPORTED_FEATURE | Threads, quotes, or reply chains sent to TikTok | These are not TikTok concepts. Remove them |
RATE_LIMITED | TikTok throttled the request | Retry later |
PLATFORM_UNAVAILABLE | TikTok reported a server-side failure | Try again later |
Large videos take time
TikTok can take minutes to process large videos. The destination stays
queued until the video is ready. If TikTok is still processing after several
minutes the publish fails with MEDIA_PROCESSING_TIMEOUT.