Publish a post
publishPost to social channels. Channels open today: threads, instagram, youtube, facebook, bluesky, telegram, naver_blog, tiktok. Get accountIds from select_channels — do not guess which channel the person meant. Naver Blog caps how many posts one ID publishes; when it does, this returns 429 naver_rate_limited with retryAfterSeconds. Do not call again before that, the draft is already in Naver. Naver Blog asks which form to publish with, every post: if options.naver_blog.form is missing this returns 400 naver_form_required with forms (id, name, when). The form is the house style to use when the person has no style of their own, so read the conversation first. If they dictated the structure or you laid it out yourself, pass options.naver_blog.layout: "as-is" and the markdown goes out unchanged. Otherwise show them the forms (naver_layout with forms: "all" lays every one out side by side) and call again with the one they pick. Do not pick for them. Naver Blog goes out through the user's browser extension. If the response carries a bridge object, read bridge.state: offline means that Chrome is closed and the post is queued (up to 7 days); logged_out means Chrome is on but not logged in to Naver; login_needed means the Naver login saved for that blog in that Chrome is signed out, so its posts wait until the person logs in again from the dashboard. Either way relay bridge.userMessage to the person word for word, do not say it was published, and know that delete_post cancels a queued post and update_post rewrites it before it goes out. If the extension in that Chrome is too old for what you asked (update_post), this returns 422 extension_outdated with the installed and required versions. Uplika asks that Chrome to update at the same moment, so follow the message: usually call again in a minute or two. Text limits differ per channel: Threads 500 characters, Instagram 2200 characters, YouTube 5000 UTF-8 bytes, Facebook 63206 characters, Bluesky 300 graphemes and 3000 UTF-8 bytes, Telegram 4096 characters (1024 with media attached), Naver Blog 30000 characters, TikTok 2200 characters. Over the limit nothing goes out to any channel, so shorten it before calling. Images and video both work on the channels that take them. How several items sit in one post differs per channel: Threads groups up to 20 items in one post, Instagram groups up to 10 items in one post, YouTube takes 1 video and no images, Facebook groups up to 10 items in one post, Bluesky has no carousel and places up to 4 images in the post itself, Telegram groups up to 10 items in one post, Naver Blog has no carousel and places up to 40 images in the post itself, TikTok groups up to 35 items in one post. More than a channel takes is not refused: the first items up to its limit go out and that target's warning says what was left out. A channel that cannot mix images and video keeps the video. YouTube is different: it takes exactly one video, no images, and it needs options.youtube.title. Its description is measured in UTF-8 bytes, so Korean and Japanese cost three per character. Instagram cannot publish text alone: every post needs at least one image or video. A single video becomes a reel there. Set options.instagram.contentType to story for a story; stories show no caption. Non-JPEG images are converted for Instagram automatically. Instagram feed images (single or carousel) must be between 4:5 and 1.91:1: one outside that range is not refused, it is center-cropped to the nearest of the two on a copy, and that target's warning says so. A 3:4 phone photo loses about 6% of its height; a 9:16 image loses 30%, so crop it yourself or send it as a story if the edges matter. A reel takes music, or another reel's sound, through options.instagram.audio with an id from search_instagram_audio (Instagram accounts connected via Facebook only); for a voice-over set volume around 20 and keep videoVolume at 100, and pick audio at least as long as the reel because it plays once from its start. Facebook publishes to a Page, never a personal profile. options.facebook.link makes a link post (no media alongside), and a single video becomes a reel (3-90 seconds). Bluesky counts graphemes, not characters, and also caps UTF-8 bytes, so a post of 300 emoji can fail on the byte limit. Set options.bluesky.langs to the language of the text (1-3 BCP-47 codes like ["ko"]): without it the post never appears in language-scoped feeds, and Bluesky has no post editing to fix it later. Links, @mentions and #hashtags in the text are made clickable for you, and a link gets a preview card, so write the URL plainly. There are three ways to get a media id, pick by where the file is: media_from_url when it already has a public https address, media_upload_link when it is on the person's own device, media_presign plus media_complete when you can PUT the bytes yourself. Then pass the media ids here. On Naver Blog the body can also place media itself with and @video(media:), each on a line of its own. Ids you reference that way are picked up even if you leave them out of mediaIds, and media you pass but never reference goes at the end of the post. Those references only work when every target is Naver Blog: other channels would publish the markup as literal text, so we refuse instead. A photo line takes {.link=https://…} to make the photo a link (describe_grammar topic=media). This publishes immediately unless you pass scheduledAt (we hold the post and send it at that time, on every channel) or draft: true (nothing goes out; the person or update_post finishes it later). scheduledAt needs a timezone offset, 10 minutes to a year out; ask the person which timezone they mean instead of guessing. options.naver_blog.scheduledAt means the same as scheduledAt on the post (we hold the post and send it then; Naver's own reservation is not used), so pass the post-level one. A scheduled or draft post comes back with status scheduled or draft, can be changed with update_post, sent early with publish_now, and dropped with delete_post. Every channel also takes options..content to send that channel a different text than the shared content, which is how Naver Blog Markdown and a 500-character Threads post fit in one call. On YouTube we pass privacyStatus through as you set it and report back what YouTube actually applied, so read the warning on the result instead of promising the person a visibility we did not confirm. Returns while the post is still publishing. The permalink is null at that moment. Call get_post with the returned id to see the final status and link. Pass wait: true to hold the response until it is really out — then you can tell the person it is posted instead of guessing. For a long post use threadItems instead of publish-then-reply: we keep the order and wait for each piece to land before sending the next one.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| wait | No | Hold the response until the post is really out. Text waits up to 10 seconds, posts with media up to 75 seconds. If it is still going after that you get the usual publishing response and should poll get_post. Defaults to false. Ignored with scheduledAt or draft. | |
| draft | No | true saves the post as a draft on uplika without sending anything. Use it when the person wants to review before it goes out. They finish it in the dashboard, or you call update_post and publish_now. | |
| batchId | No | Optional tag (letters, digits, - or _) to group posts made together, for example the same text sent to two workspaces. Pass the same value on each call. | |
| content | No | Post text | |
| options | No | Per-channel settings, keyed by channel id. Only the channels you are posting to need an entry. Every channel takes content to override the shared text for that channel alone. | |
| mediaIds | No | Media ids from media_presign (confirmed with media_complete), media_from_url or media_upload_link. On Naver Blog you can leave out ids the body already points at with media:<id>; we pick those up from the text. | |
| topicTag | No | One topic to tag the post with, like a category. Only some channels take one, and those reject periods and ampersands in it. If any channel in accountIds does not take topics the whole call is refused, so publish to it separately. On a thread it goes on the first piece only. Leave it out unless the person asked for a topic. | |
| accountIds | Yes | Account ids from select_channels | |
| automation | No | Attach an automation to this post in the same call. template defaults to comment_to_dm; params are that template's params (see list_automation_templates) minus post, which is this post. Created as a draft unless enabled: true. Each target goes through the same checks as create_automation. A target whose channel the template does not cover (comment_to_dm on Threads or Naver Blog, any template on YouTube, Telegram, Bluesky or TikTok) is skipped and named in the response with the reason. | |
| scheduledAt | No | ISO 8601 time with a timezone offset (2026-09-20T09:00:00+09:00) to send the post at, on every channel. 10 minutes to 365 days from now. We keep the post as scheduled until then; the person can still change or cancel it. | |
| threadItems | No | Split a long post into a chain instead of calling publish and then reply. The first item is the root and the rest become replies under it, in order. We handle the ordering and the waiting. Each item obeys the character limit on its own. Pass either content or threadItems, not both. If a later item fails, the ones already up stay up and the response tells you where to resume. | |
| workspaceId | No | Which workspace this is for. Only needed when the account has more than one — the error tells you the ids when it matters. Leave it out if it is already decided; do not ask the person again. |