REST API
Platforms
Every channel and its rules, read from the same source we validate against.
This endpoint is the honest answer to 'what are the limits'. It is the same table the validator uses, so it cannot drift from behaviour.
GET/v1/platforms
Every channel, its limits, and whether it is live.
Limits today
| Name | Type | Description |
|---|---|---|
Threads | 500 | images 20 · video 1 · text alone ok · live |
Instagram | 2,200 | images 10 · video 1 · media required · live |
YouTube | 5,000 + title 100 | video 1 · media required · live |
Facebook | 63,206 | images 10 · video 1 · text alone ok · live |
Bluesky | 300 + 3,000 bytes | images 4 · video 1 · text alone ok · live |
Telegram | 4,096 · 1,024 with media | images 10 · video 10 · text alone ok · live |
Naver Blog | 30,000 + title 100 | images 40 · video 10 · text alone ok · beta, through the browser extension |
TikTok | 2,200 + title 90 | images 35 · video 1 · media required · in platform review, testers only |
How a character is counted
Every character counts as 1, except code points above U+FFFF (mostly emoji) which count as 4. Do not use string length: "a😀".length is 3 but it counts as 5. GET /v1/platforms returns the same rule per channel as charCount, so you can check before publishing instead of after it fails.
Your own limits come from GET /v1/me
This table is what each channel allows. What your account allows is a different number, and GET /v1/me returns it as limits: how many workspaces you can create and how many requests a minute you get. There is no separate cap on connections. A workspace holds one channel per platform, and connecting a second account on the same platform is refused with platform_already_connected. **These are beta values and they will change.** Read them at runtime instead of writing them into your code.
What options.<channel> takes
Every channel entry carries options: a JSON Schema of the per-channel settings publish accepts, generated from the same adapter the server validates with. Read it instead of guessing field names. Fields marked agentOnly are for API callers; the dashboard does not draw them. Every channel also takes content, a different text for that channel alone. The tables below are that schema, rendered.
options.threads
| Name | Type | Description |
|---|---|---|
topicTag | string | One topic to tag the post with, like a category. Threads takes exactly one and rejects periods and ampersands in it. On a thread it goes on the first piece only. Leave it out unless the person asked for a topic. |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
options.instagram
| Name | Type | Description |
|---|---|---|
contentType | feed | story | reel | What kind of post. Defaults to reel for a single video and feed otherwise. Stories show no caption and take one image or video. |
shareToFeed | boolean | Reels only. false keeps the reel out of the main feed. |
coverMediaId | string | Media id of a JPEG up to 8MB to use as the reel cover. Reels only. |
thumbOffsetMs | number | Reel cover frame in milliseconds. Ignored when coverMediaId is set. |
collaborators | string[] | Up to 3 public professional accounts to invite as collaborators. They appear only after accepting. Not on stories. |
userTags | object[] | Tag accounts on the media. Photos need x and y between 0 and 1; videos take the username alone. mediaIndex picks the carousel slide. (API only; the dashboard does not draw it.) |
isAiGenerated | boolean | Set true to label the media as AI-generated on Instagram. An attached media marked aiGenerated turns this on automatically unless you set it to false. |
firstComment | string | A comment we post right after publishing, often used for hashtags. Not on stories. If it fails the post still goes up, with a warning. |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
options.youtube
| Name | Type | Description |
|---|---|---|
titleRequired | string | Video title, up to 100 characters. Required. Cannot contain < or >. |
defaultLanguage | string | The language the title and description are written in, as one BCP-47 code (for example en-US or ko). Leave it out and none is sent. The spoken language (YouTube Studio's Video language) cannot be set through the API. |
tags | string[] | Search tags. 500 characters across all of them; a tag with a space costs two extra for the quotes YouTube adds. |
categoryId | string | YouTube category id. Defaults to 22 (People & Blogs). Leave it out unless the person named a category. (API only; the dashboard does not draw it.) |
privacyStatus | public | unlisted | private | Defaults to private. We pass this through as you set it. If YouTube applies something else, the result carries a warning saying what it actually applied. |
publishAt | string | ISO 8601 time to make the video public. Only works with privacyStatus private, and only on a video that was never public. This is YouTube's own scheduling; for scheduling across channels use scheduledAt on the post. (API only; the dashboard does not draw it.) |
madeForKids | boolean | Whether this video is directed at children. This is a legal declaration about someone else's channel. Leave it out unless the person tells you, and the channel's own default applies. If the channel has not set its audience either, YouTube Studio will not save other edits to this video until someone picks one, so ask the person. |
containsSyntheticMedia | boolean | Set true when the video contains realistic altered or synthetic content, including AI generated footage of real-looking people, places or events. An attached media marked aiGenerated turns this on automatically unless you set it to false. |
thumbnailMediaId | string | Media id of a JPEG or PNG up to 2MB to use as the thumbnail. Long-form only: YouTube does not take custom thumbnails on Shorts, and a vertical video of three minutes or less becomes a Short. The channel also has to be verified before YouTube accepts one. |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
options.facebook
| Name | Type | Description |
|---|---|---|
contentType | feed | story | reel | What kind of post. Defaults to reel for a single video and feed otherwise. A Facebook story is rejected if you send any text: publish the story with no content, and put the words in a feed post instead. A story also takes exactly one image or video and no first comment. |
link | string | Makes a link post with a preview card. Cannot be combined with media. |
title | string | Reel title, separate from the caption. |
scheduledPublishTime | string | ISO 8601 time to publish a feed post later, 10 minutes to 28 days from now. Feed posts only, not reels or stories. Cannot be combined with firstComment (a scheduled post is not live yet, so nothing to comment on). This is Facebook's own scheduling; for scheduling across channels use scheduledAt on the post. (API only; the dashboard does not draw it.) |
firstComment | string | A comment we post right after publishing. Not on stories, and not on scheduled posts. If it fails the post still goes up, with a warning. |
feedTargeting | object | Preferred audience for a Facebook text or link post. A hint, not a limit: Facebook may show the post more to these people, others can still see it, and the effect is not guaranteed. Facebook does not keep it on photo posts or reels (we checked), so those are refused. It cannot be changed after the post is published. (API only; the dashboard does not draw it.) |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
options.bluesky
| Name | Type | Description |
|---|---|---|
langs | string[] | The language of the text, 1-3 BCP-47 codes like ["ko"] or ["en", "ko"]. Feed generators filter on this, so without it the post never appears in language-scoped feeds. Bluesky has no post editing, so this cannot be fixed after publishing. Set it to the language the text is actually written in. |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
options.telegram
| Name | Type | Description |
|---|---|---|
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
options.naver_blog
| Name | Type | Description |
|---|---|---|
titleRequired | string | Required. The post title, up to 100 characters. |
categoryId | string | The id of a category on the connected blog. list_accounts returns them under naverBlog.categories. Give this or category (the name); there is no default, and without one Naver files the post under the wrong board. Not needed and ignored when draftOnly is true (a draft has no board until it is published). |
category | string | The category name instead of its id, exactly as it appears in naverBlog.categories from list_accounts. If the blog does not have it you get naver_category_unknown with the list to pick from; if the person just created it, call refresh_account first. (API only; the dashboard does not draw it.) |
tags | string[] | Up to 30 tags without the # sign and without spaces. Ignored when draftOnly is true. |
openType | public | neighbor | mutual | private | Who can see it. public (default), neighbor, mutual (mutual neighbors only), or private. Ignored when draftOnly is true. |
draftOnly | boolean | Save as a draft in Naver instead of publishing. The user finishes it by hand, or you publish it later with publish_naver_draft. categoryId, tags and openType are not stored on a draft; pass them when you publish it. Different from draft on the post, which keeps the whole post on uplika. |
layout | template | as-is | template (default) reshapes the body into the blog's house form before it goes out: #/## titles become underlined quote headings, a thin rule sits between sections, and photos you did not place with media: references go one per section (leftovers pair up at the end). Text is never changed. Call naver_layout first to see the result. Use as-is when the person dictated the structure or you placed everything yourself. (API only; the dashboard does not draw it.) |
form | bubble-info | divider-tutorial | photo-review | place-list | news-event | table-explain | mood-review | photo-story | Form preset for the template layout (default photo-story). GET /v1/naver/forms lists the presets; naver_layout with forms: "all" shows them side by side. |
scheduledAt | string | Same as scheduledAt on the post: ISO 8601 with a timezone offset, 10 minutes to a year out. Uplika holds the post and sends it to Naver at that time; Naver's own reservation is not used. Prefer scheduledAt on the post. Not allowed together with draftOnly or on a post that also targets other channels. (API only; the dashboard does not draw it.) |
tempLogNo | string | Set by publish_naver_draft: the Naver temp-saved draft to publish as it is. The body is ignored; the extension loads that draft in the editor and presses publish. (API only; the dashboard does not draw it.) |
fontFamily | nanumgothic | nanummyeongjo | nanumbarungothic | nanumsquare | maruburi | Body font, one of nanumgothic, nanummyeongjo, nanumbarungothic, nanumsquare, maruburi. Omit for the editor default. |
fontSize | 11 | 13 | 15 | 16 | 19 | 24 | 28 | 30 | 34 | 38 | Body font size in px, one of the editor's sizes (11-38). Omit for the editor default. |
alignCenter | boolean | Center every paragraph and image that has no alignment of its own. A photo line's own {.left} / {.center} / {.right} wins. |
imageSize | fit | small | Size of every single photo that has none of its own: fit is the document width, small is the editor's Small (about three quarters). A photo line's own {.fit} / {.small} wins. Leave it out for the default: document width for photos wider than 693px, original width for narrower ones. @group photos are always document width. Needs uplika extension 0.7.13 or later; with an older one the post still goes out and, if fit was asked for, a warning says so. |
seoKeywords | object | The search keywords this post targets, from keyword research (POST /v1/naver/keywords/judge): main is the one keyword the title leads with, subs (2-5) become the section titles one each. Not used when publishing; stored with the post and read by the SEO check. |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. On Naver Blog this is where the Markdown body goes when the other channels get plain text. |
options.tiktok
| Name | Type | Description |
|---|---|---|
privacyLevel | string | Required unless postMode is draft. One of the values that get_publish_options returns for this account, e.g. PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR or SELF_ONLY. The list differs per account, so do not hard-code it. |
title | string | Required for a photo post, up to 90 characters. A video post has no title field at all and passing one is rejected; a video's text comes from content. |
postMode | direct | draft | direct (default) posts to the profile. draft sends it to the creator's TikTok inbox; they open the notification and finish it in the app, choosing who can see it themselves, so we send no visibility on this path. Until Uplika completes TikTok's review, a direct post is visible only to the creator (SELF_ONLY). A draft is not on the profile until the person finishes it, so there is no link and no metrics in the meantime. |
disableComment | boolean | Turn comments off for this post. Cannot be set to false if the creator disabled comments account-wide. |
disableDuet | boolean | Turn duets off for this post. Video posts only. |
disableStitch | boolean | Turn stitches off for this post. Video posts only. |
brandContentToggle | boolean | Declare paid partnership content ("Branded Content"). TikTok does not allow branded content to be private, so this cannot be combined with privacyLevel SELF_ONLY. |
brandOrganicToggle | boolean | Declare that the post promotes the creator's own brand ("Your Brand"). Leave it out rather than sending false: not declaring and declaring 'no' are different statements. |
isAigc | boolean | Declare that the content was generated by AI. Worth setting when you made the video or images. An attached media marked aiGenerated turns this on automatically unless you set it to false. |
videoCoverTimestampMs | number | Which frame to use as the cover, in milliseconds into the video. TikTok does not accept a cover image file, only a timestamp. |
content | string | Text for this channel only. When set it replaces the shared content for this channel, and the length and media checks use it. Leave it out to use the shared content. |
Common questions
What is the character limit on each channel?
The table above has it, and it is generated from the same table the validator reads. If a limit changes in the product it changes here in the same commit, so this page cannot drift from behaviour.
What happens if I go over a limit?
The call is refused with content_too_long before anything is published, and the response carries the channel, the limit and your actual length so the caller can shorten and retry.
Do I have to pick the strictest limit myself?
Only if you want to. Publishing to several channels at once checks each one against its own limit, so you find out which channel refused rather than guessing at a single number.