uplika
Registry code: 98e063576c18cde7
Uplika publishes to the social channels a person has connected to their Uplika account, and reads replies and metrics back.
Start with list_accounts or select_channels. Never assume a channel is connected; if it is not, say so and send the person to https://uplika.com/dashboard/connections.
- endpoint
- https://api.uplika.com/mcp
- protocol
- streamable-http ·2025-06-18
- authentication
- bearer
- public key
- none — nobody has proven they own this listing · is it yours? claim it
- karma
- 0 · newcomer
- Is uplika live?
- Yes — it answered the hub's last check (checked 2h ago). It answered 99% of checks over the last 30 days.
- Is uplika free to use?
- Not anonymously — it asks for a key or a login before it will serve. Whether that key costs anything is the operator's plan to say: the hub measured the door, not the pricing.
- What tools does uplika have?
- 61 tools: refresh_account, send_to_past_comments, list_accounts, list_posts, list_channel_posts, publish, get_post, retry_post, ….
- Is uplika safe to connect?
- The hub found no text in its card or tool descriptions aimed at the agent reading them. It measures what the server answers, not its code — grant it only the access its tools need.
90 days 99.2%· all time 99.2%
last good check
of 61 tools
- down → live
- degraded → down· HTTP 502
- live → degraded· HTTP 502
- degraded → live
- unknown → live
- topic
- communication social media
- used for
- publish posts to connected social channels
- reply to comments
- read dm conversations
- manage social automations
- research naver blog keywords
- takes → gives
- text, images, video → text, data
- tools
- 15 reads8 changes data2 sends messages
Calls placed through this hub's router, from its own receipts. Every caller and every payer counts the same; the chain total is counted from three payers.
through this hub
successful
what callers paid
Price is per tool, not per server. An agent whose handshake is open can hold tools that demand a key or a payment, and one figure for the whole agent sends callers into a wall.
refresh_account auth-required never probed
Re-read one connected channel's metadata. On Naver Blog this re-reads the blog's categories through the user's browser extension and waits up to a minute for it; list_accounts then shows the new list under naverBlog.categories. Call this when a category the person mentions is not in list_accounts yet. If the extension is offline you get 202 and the refresh runs when that browser comes back. On Threads, Instagram and Facebook this re-reads the display name and the profile picture. It does not renew a sign-in: a connection whose sign-in the channel has ended answers account_expired, and that account is reconnected on the Connections page. Channels with nothing to re-read return refresh_unsupported. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "accountId" ], "properties": { "accountId": { "type": "string", "description": "The account id from list_accounts." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linessend_to_past_comments auth-required never probed
Run a live comment automation on comments that are **already on the post**: the ones it missed because they came before it was enabled, during an outage, or while another tool handled the account. An automation normally reacts only to new comments. How Meta works: a comment can get one private reply, and only within 7 days. Older comments cannot be reached, and a comment another app already answered by DM is refused by Meta (counted as alreadyReplied, not as a failure). Instagram and Facebook only; top-level comments only. mode preview sends nothing: it reads the newest comments and answers eligible (how many would get the automation now) and skipped by reason (mine, tooOld, keyword, alreadyRan, answered, sameAuthor). One person gets it once: when someone commented several times, only their newest comment is answered (sameAuthor counts the rest). **Always preview first, show the person the numbers, and start only after they confirm**, because a sent message cannot be recalled. mode start queues the job: comments are answered a few at a time (Meta allows 750 private replies per hour per account, shared with the live automation), so a large post takes hours. Read progress in get_automation (pastComments), and see each answered comment in list_automation_runs. mode stop halts a running job; disabling the automation stops it too. By default a comment that already has a reply from the account is skipped (answered) and the trigger's public reply is not posted; includeAnswered and publicReply change that. The automation must be live (automation_not_live); one job per automation at a time (past_comments_running); a next-post automation that has not bound to a post yet has nothing to read (past_comments_no_post).
{ "type": "object", "required": [ "id", "mode" ], "properties": { "id": { "type": "string", "description": "The automation." }, "mode": { "enum": [ "preview", "start", "stop" ], "type": "string", "description": "preview counts without sending, start queues the sends, stop halts a running job." }, "publicReply": { "type": "boolean", "description": "Also post the trigger's public reply under each comment. Default false, so an old post does not get the same reply many times at once." }, "workspaceId": { "type": "string", "description": "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." }, "includeAnswered": { "type": "boolean", "description": "Also send to comments that already have a reply from the account. Default false." } } }arguments 34 lineslist_accounts reads auth-required 46d ago
Connected social accounts. **Call this before publishing anything.** Each item has id, platform (threads etc.), handle and status. The accountIds you pass to publish are these ids, and only "active" ones publish. If the person did not name a channel, target every active account. If the list is empty, no channel is connected yet. Send the person to https://uplika.com/dashboard/connections.
{ "type": "object", "properties": { "workspaceId": { "type": "string", "description": "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." } } }arguments 9 lineslist_posts auth-required never probed
Recent publishes made through us and the per-target status of each, scheduled and draft posts included (their status says so and scheduledAt says when). Newest first, 20 by default. Pass limit for more or fewer, up to 100. hasMore means the list was cut short; pass the returned nextBefore as before to keep going. To answer "what is scheduled this week", pass status scheduled with since and until and sort scheduled. Posts that already existed on the channel are not here. Use list_channel_posts for those.
{ "type": "object", "properties": { "sort": { "enum": [ "scheduled" ], "type": "string", "description": "scheduled orders by the post's date ascending (soonest first) and drops paging. Omit for newest first." }, "limit": { "type": "number", "description": "1-100, defaults to 20" }, "since": { "type": "string", "description": "YYYY-MM-DD or ISO 8601. Only posts dated at or after this. A scheduled post is dated by its scheduledAt, a sent post by when it was created." }, "until": { "type": "string", "description": "YYYY-MM-DD or ISO 8601. Only posts dated at or before this (a date means the whole day)." }, "before": { "type": "string", "description": "A publish id from a previous page's nextBefore. Returns the ones older than it." }, "status": { "type": "string", "description": "Narrow to one or more statuses, comma separated: scheduled, draft, publishing, published, partial, failed or cancelled. Omit for everything." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 36 lineslist_channel_posts auth-required never probed
What is actually on the channel right now, including posts written in the channel's own app. Use this to find a post when you do not have its link. Each item carries a permalink you can pass straight to open_post, reply or delete_post. An empty list does not always mean the account has no posts: on TikTok this reads public videos only, so videos set to Only me, videos still waiting in the creator's TikTok inbox, and videos posted before Uplika passed TikTok's audit (2026-10-09) do not appear. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "properties": { "limit": { "type": "number", "description": "1-100, defaults to 25" }, "accountId": { "type": "string", "description": "Limit to one account. Omit to cover every connected channel." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 17 linespublish auth-required never probed
Post 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. 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:<id>), 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.<channel>.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.
{ "type": "object", "required": [ "accountIds" ], "properties": { "wait": { "type": "boolean", "description": "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": { "type": "boolean", "description": "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": { "type": "string", "description": "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": { "type": "string", "description": "Post text" }, "options": { "type": "object", "properties": { "tiktok": { "type": "object", "properties": { "title": { "type": "string", "maxLength": 90, "description": "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." }, "isAigc": { "type": "boolean", "description": "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." }, "content": { "type": "string", "description": "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." }, "postMode": { "enum": [ "direct", "draft" ], "type": "string", "description": "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. A draft is not on the profile until the person finishes it, so there is no link and no metrics in the meantime." }, "disableDuet": { "type": "boolean", "description": "Turn duets off for this post. Video posts only." }, "privacyLevel": { "type": "string", "description": "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." }, "disableStitch": { "type": "boolean", "description": "Turn stitches off for this post. Video posts only." }, "disableComment": { "type": "boolean", "description": "Turn comments off for this post. Cannot be set to false if the creator disabled comments account-wide." }, "brandContentToggle": { "type": "boolean", "description": "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": { "type": "boolean", "description": "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." }, "videoCoverTimestampMs": { "type": "number", "minimum": 0, "description": "Which frame to use as the cover, in milliseconds into the video. TikTok does not accept a cover image file, only a timestamp." } }, "description": "TikTok settings. privacyLevel is REQUIRED for a normal post and has no default on purpose: TikTok's rules say the person must choose visibility deliberately, so we ask for it instead of guessing. Call get_publish_options first to see which values this account may use right now, whether it can post at all, and its video length limit. TikTok has no text-only posts, cannot mix a video and photos, and has no API for deleting a post or reading comments. A direct post goes out with the privacyLevel you pass; only a public post that TikTok has reviewed comes back with a link and metrics. postMode draft sends the video to the creator's TikTok inbox, where they choose who can see it." }, "bluesky": { "type": "object", "properties": { "langs": { "type": "array", "items": { "type": "string", "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$", "description": "One BCP-47 code." }, "maxItems": 3, "description": "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": { "type": "string", "description": "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." } }, "description": "Bluesky settings. Optional, but langs is worth setting on every post." }, "threads": { "type": "object", "properties": { "content": { "type": "string", "description": "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." }, "topicTag": { "type": "string", "maxLength": 64, "description": "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." } }, "description": "Threads settings. Optional." }, "youtube": { "type": "object", "required": [ "title" ], "properties": { "tags": { "type": "array", "items": { "type": "string", "description": "One search tag." }, "description": "Search tags. 500 characters across all of them; a tag with a space costs two extra for the quotes YouTube adds." }, "title": { "type": "string", "maxLength": 100, "description": "Video title, up to 100 characters. Required. Cannot contain < or >." }, "content": { "type": "string", "description": "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." }, "publishAt": { "type": "string", "description": "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." }, "categoryId": { "type": "string", "description": "YouTube category id. Defaults to 22 (People & Blogs). Leave it out unless the person named a category." }, "madeForKids": { "type": "boolean", "description": "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." }, "privacyStatus": { "enum": [ "public", "unlisted", "private" ], "type": "string", "description": "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." }, "defaultLanguage": { "type": "string", "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$", "description": "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." }, "thumbnailMediaId": { "type": "string", "description": "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." }, "containsSyntheticMedia": { "type": "boolean", "description": "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." } }, "description": "Required when a YouTube account is in accountIds. The post content becomes the video description." }, "facebook": { "type": "object", "properties": { "link": { "type": "string", "description": "Makes a link post with a preview card. Cannot be combined with media." }, "title": { "type": "string", "description": "Reel title, separate from the caption." }, "content": { "type": "string", "description": "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." }, "contentType": { "enum": [ "feed", "story", "reel" ], "type": "string", "description": "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." }, "firstComment": { "type": "string", "description": "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": { "type": "object", "properties": { "countries": { "type": "array", "items": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "ISO 3166-1 two-letter country code in capitals, like US." }, "maxItems": 25, "description": "Countries to aim the post at, for example [\"US\", \"GB\"]." } }, "description": "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." }, "scheduledPublishTime": { "type": "string", "description": "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." } }, "description": "Facebook Page settings. Optional: plain text publishes without it." }, "telegram": { "type": "object", "properties": { "content": { "type": "string", "description": "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." } }, "description": "Telegram has no channel-specific settings. Note the caption limit: with media attached the text is capped at 1,024 characters instead of 4,096." }, "instagram": { "type": "object", "properties": { "content": { "type": "string", "description": "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." }, "userTags": { "type": "array", "items": { "type": "object", "required": [ "username" ], "properties": { "x": { "type": "number", "description": "0 to 1 from the left. Photos only." }, "y": { "type": "number", "description": "0 to 1 from the top. Photos only." }, "username": { "type": "string", "description": "Username without @." }, "mediaIndex": { "type": "number", "description": "Which carousel slide, from 0." } }, "description": "One tagged account." }, "description": "Tag accounts on the media. Photos need x and y between 0 and 1; videos take the username alone. mediaIndex picks the carousel slide." }, "contentType": { "enum": [ "feed", "story", "reel" ], "type": "string", "description": "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": { "type": "boolean", "description": "Reels only. false keeps the reel out of the main feed." }, "coverMediaId": { "type": "string", "description": "Media id of a JPEG up to 8MB to use as the reel cover. Reels only." }, "firstComment": { "type": "string", "description": "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." }, "collaborators": { "type": "array", "items": { "type": "string", "description": "A public professional account's username, without @." }, "maxItems": 3, "description": "Up to 3 public professional accounts to invite as collaborators. They appear only after accepting. Not on stories." }, "isAiGenerated": { "type": "boolean", "description": "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." }, "thumbOffsetMs": { "type": "number", "minimum": 0, "description": "Reel cover frame in milliseconds. Ignored when coverMediaId is set." } }, "description": "Instagram settings. Optional: a plain post with media works without it." }, "naver_blog": { "type": "object", "required": [ "title" ], "properties": { "form": { "enum": [ "bubble-info", "divider-tutorial", "photo-review", "place-list", "news-event", "table-explain", "mood-review", "photo-story" ], "type": "string", "description": "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." }, "tags": { "type": "array", "items": { "type": "string", "description": "One tag without # and without spaces." }, "maxItems": 30, "description": "Up to 30 tags without the # sign and without spaces. Ignored when draftOnly is true." }, "title": { "type": "string", "maxLength": 100, "description": "Required. The post title, up to 100 characters." }, "layout": { "enum": [ "template", "as-is" ], "type": "string", "description": "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." }, "content": { "type": "string", "description": "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." }, "category": { "type": "string", "description": "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." }, "fontSize": { "enum": [ "11", "13", "15", "16", "19", "24", "28", "30", "34", "38" ], "type": "number", "maximum": 34, "minimum": 10, "description": "Body font size in px, one of the editor's sizes (11-38). Omit for the editor default." }, "openType": { "enum": [ "public", "neighbor", "mutual", "private" ], "type": "string", "description": "Who can see it. public (default), neighbor, mutual (mutual neighbors only), or private. Ignored when draftOnly is true." }, "draftOnly": { "type": "boolean", "description": "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." }, "imageSize": { "enum": [ "fit", "small" ], "type": "string", "description": "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." }, "tempLogNo": { "type": "string", "description": "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." }, "categoryId": { "type": "string", "description": "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)." }, "fontFamily": { "enum": [ "nanumgothic", "nanummyeongjo", "nanumbarungothic", "nanumsquare", "maruburi" ], "type": "string", "description": "Body font, one of nanumgothic, nanummyeongjo, nanumbarungothic, nanumsquare, maruburi. Omit for the editor default." }, "alignCenter": { "type": "boolean", "description": "Center every paragraph and image that has no alignment of its own. A photo line's own {.left} / {.center} / {.right} wins." }, "scheduledAt": { "type": "string", "description": "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." }, "seoKeywords": { "type": "object", "properties": { "main": { "type": "string", "maxLength": 100, "description": "The main keyword, verbatim in the title and the first line." }, "subs": { "type": "array", "items": { "type": "string", "maxLength": 100, "description": "One sub keyword." }, "maxItems": 5, "description": "Sub keywords, one per section title." }, "measuredAt": { "type": "string", "description": "ISO 8601 date the numbers were measured." } }, "description": "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." } }, "description": "Naver Blog settings. Required when a target is a Naver Blog account. Naver Blog has no official API: the post is written by the user's browser extension, so it goes out only while that browser is open. list_accounts tells you whether the extension is online; if it is offline the post queues for up to 7 days and you should say so. The body is Markdown, plus Naver-only markup for things Markdown has no word for (highlighting, styled tables, collages, maps, events): call describe_grammar with platform naver_blog for the syntax and its traps. Reference uploaded media inline with ``; ids you reference this way do not have to repeat in mediaIds, and media you pass but never reference goes at the end. When other channels are in the same post, put the Markdown in options.naver_blog.content so the shared content stays plain text." } }, "description": "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": { "type": "array", "items": { "type": "string" }, "description": "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": { "type": "string", "description": "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": { "type": "array", "items": { "type": "string" }, "description": "Account ids from select_channels" }, "automation": { "type": "object", "properties": { "name": { "type": "string" }, "params": { "type": "object" }, "enabled": { "type": "boolean" }, "template": { "type": "string" } }, "description": "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": { "type": "string", "description": "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": { "type": "array", "items": { "type": "object", "required": [ "content" ], "properties": { "content": { "type": "string" }, "mediaIds": { "type": "array", "items": { "type": "string" } } } }, "description": "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": { "type": "string", "description": "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." } } }arguments 530 linesget_post auth-required never probed
One publish: per-target status and the reason any target failed. For a post link you probably want open_post instead. A target whose account was disconnected keeps its status and link, with connectionId null and accountHandle set to that account's handle; editing, deleting or retrying the post then returns 409 account_disconnected until the same account is reconnected, and the same post id comes back with it. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linesretry_post auth-required never probed
Retry the targets that failed on a publish. Targets that already went out are left alone. 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. If a Naver target stopped right after Save as draft (errorCode naver_draft_unknown), this returns 409 naver_draft_unknown and retries nothing; check list_naver_drafts and call again with force: true only if the draft is not there. If a Naver reply did not confirm in time (errorCode naver_reply_unknown), this returns 409 naver_reply_unknown and retries nothing; look at the comment on the blog first and call again with force: true only if the reply is not there. If another post with the same content and files is queued, going out, published or scheduled on the same account, this returns 409 duplicate_post with that post's postId and retries nothing; force: true sends it anyway. If nothing failed you get nothing_to_retry. Only applies to posts published through us. This replays the same payload, so read errorCode and retryable on get_post first: when retryable is false the arguments have to change and you should call publish again instead. On Naver Blog a failed target may still have left a post or a draft in the editor, so check the blog before retrying.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "force": { "type": "boolean", "description": "Retry even though a Naver target is naver_draft_unknown (pass it only after list_naver_drafts shows the draft is not there) or naver_reply_unknown (pass it only after the comment on the blog shows no such reply), or even though another post with the same content is on the same account (duplicate_post)." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 20 linesupdate_post auth-required never probed
Change a scheduled or draft post before it goes out, or edit a post that is already live on a channel that supports editing (list_platforms features: today Naver Blog, YouTube and some Facebook posts). Fields: content, mediaIds, accountIds, options, scheduledAt or draft. Fields you leave out keep their current value (on a Naver Blog post, see media below); options you pass replace the whole options object (on a post written directly on the channel they are merged per channel key instead). scheduledAt moves the send time (same rules as publish), null turns it into a draft, and draft: true does the same. A thread (chain) can be changed too while it is scheduled or a draft: content replaces the first item and keeps the later items; threadItems replaces the whole chain (one item collapses it to a single post); passing threadItems to a single post turns it into a chain. Pass id of the first item — later items return 409 thread_piece with rootId. Posts that already went out return post_not_editable on channels that cannot edit a live post. A post written directly on the channel (one a link or id resolved to, origin imported) can be edited on YouTube, on the text of Facebook text, link and photo posts, and on Naver Blog: uplika reads the post from the channel first, so fields you leave out keep the channel's current values. Elsewhere it returns post_not_editable with the reason (list_platforms feature update_imported). A Naver post written in Naver's editor is rewritten from its current source, which uplika reads from Naver's edit screen: text, bold, italic, underline, strikethrough and links are markdown; headings and quotes are markdown only when their text has no style at all; every other block (photos, videos, cards, tables, headings or quotes with a font, and whole text blocks that have colors, sizes or alignment anywhere, several paragraphs each) is one @keep(...) line. A line holding only an invisible U+200B character is blank space at the edge of a text block; leave it to keep the spacing. Send the edited source as content together with sourceVersion. Without it, or when the post changed on Naver since, this returns 409 naver_source_required with the current source and its sourceVersion (get_post also shows sourceVersion, but its content is only the source after uplika has read it once). Keep a @keep line to keep that block where it is, delete it to remove the block; text you write directly next to a @keep text block joins that block, and comes back inside its @keep line on the next read. An unknown id returns 422 naver_keep_unknown. This needs extension 0.7.8 or later (422 extension_outdated otherwise). A published Naver post is rewritten in place (same URL, same logNo) when you pass content, mediaIds or options. On a post that goes only to Naver Blog, the media: references in the new body are the post's media list: when you leave mediaIds out, media the new body does not place is removed from the post and the response note says how many. Pass mediaIds only for media to add at the end of the post without placing it in the body. If the body places more photos or videos than Naver takes (list_platforms), this returns 422 naver_media_over_limit with droppedIds and nothing changes. 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. If the Naver editor shows a "missing image" error when you open an already-published post for editing, calling update_post on that post rewrites it in place and fixes it; the URL stays the same. A Naver draft (a target whose externalId starts with draft:, from options.naver_blog.draftOnly) is not on the blog: update_post publishes uplika's copy as a new post and, with extension 0.7.2 or later, removes the old draft from Naver's draft box. To publish the draft exactly as it is in Naver, call publish_naver_draft. Editing a post that is already live is asynchronous: pass wait: true to hold the response for the result (up to 75 seconds), or read it with get_post. If the account that published the post was disconnected, this returns 409 account_disconnected; reconnect the same account and the post id comes back.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "wait": { "type": "boolean", "description": "Only when editing a post that is already live: hold the response until the edit has gone through or not, up to 75 seconds. Past that it returns while still working, with next telling you to read get_post." }, "draft": { "type": "boolean", "description": "true turns a scheduled post back into a draft." }, "content": { "type": "string", "description": "New post text." }, "options": { "type": "object", "description": "Per-channel settings, same shape as on publish. Replaces the whole object." }, "mediaIds": { "type": "array", "items": { "type": "string" }, "description": "New media, in order. Replaces the current set. On a Naver Blog post the body's media: references already are the list, so leave this out unless you add media to the end of the post without placing it." }, "accountIds": { "type": "array", "items": { "type": "string" }, "description": "New target accounts, from select_channels. Replaces the current set." }, "scheduledAt": { "type": [ "string", "null" ], "description": "New send time (ISO 8601 with offset, 10 minutes to 365 days out), or null to keep the post as a draft instead." }, "threadItems": { "type": "array", "items": { "type": "object", "required": [ "content" ], "properties": { "content": { "type": "string" }, "mediaIds": { "type": "array", "items": { "type": "string" } } } }, "description": "Replace the whole chain: [{ content, mediaIds? }], first item is the root. Only while the post is scheduled or a draft, and only for Threads and Bluesky. Leave it out to keep the current items (content then edits just the first one). Exclusive with content." }, "workspaceId": { "type": "string", "description": "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." }, "sourceVersion": { "type": "string", "description": "Only for a Naver post written in Naver's editor: the sourceVersion that came with the source you edited (from naver_source_required or get_post). It tells uplika the content was made from the post's current source." } } }arguments 78 lineshide_reply auth-required never probed
Hide a reply on the channel, or show it again with hide: false. The reply id comes from list_replies, and postId is the publish it belongs to.
{ "type": "object", "required": [ "replyId", "postId" ], "properties": { "hide": { "type": "boolean", "description": "true hides it, false shows it again. Defaults to true." }, "postId": { "type": "string", "description": "The post the reply sits under. A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "replyId": { "type": "string", "description": "Reply id from list_replies" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 25 linesdescribe_grammar auth-required 42d ago
How to write the body for a channel that has its own markup. Naver Blog has one; every other platform returns not_supported, which is not an error to work around. Call this before writing a Naver Blog body for the first time, or whenever you want something the basics do not cover: highlighting a phrase, a styled table, a collage, an event block, a map with several places. Without a topic you get an overview and the list of topics; with one you get that section in full, including the mistakes that fail silently. The values come from the same grammar the publisher validates against, so what this returns is what publish accepts.
{ "type": "object", "required": [ "platform" ], "properties": { "topic": { "enum": [ "overview", "directives", "attributes", "inline", "blocks", "highlight", "media", "limits" ], "type": "string", "description": "Which section. overview (default) sketches the whole thing; directives, attributes, inline, blocks, highlight, media and limits go deep." }, "platform": { "type": "string", "description": "Platform id, e.g. naver_blog. Only naver_blog has body markup today." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 30 lineslist_naver_drafts auth-required 45d ago
Naver Blog only. Lists the drafts (temp-saved posts) sitting in the blog's draft box: logNo, title and the last-saved time, newest first. A post you published with options.naver_blog.draftOnly is one of them, and so is anything the person saved by hand in the Naver editor. Pass a logNo from here to publish_naver_draft. Needs the extension 0.7.2 or later in the person's Chrome; otherwise you get extension_outdated. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "properties": { "accountId": { "type": "string", "description": "Limit to one Naver Blog account. Omit to cover every connected Naver Blog account." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 13 linesdiagnose_naver_blog auth-required 42d ago
Naver Blog only. Reads a blog's public RSS feed (its latest posts, at most 50) and says whether the titles are written for search: the share of titles carrying a search intent word (price, how to, review), how many start with a date or episode label, posts per month, the categories, and the words the blog repeats in titles (seedCandidates: candidates to research, not proven keywords). summary.oldest and summary.newest say which dates it read, so a quiet blog's 50 posts are not everything; summary.capped is true at 50. flags and thresholds carry the judgement (lowIntent when the intent share is under thresholds.lowIntentPct). Works for any public blog, not only connected ones, and needs no Naver keys. Cached 24 hours; a cache miss spends one of 20 diagnoses per person per day (quota in the response). Returns 403 research_tool_disabled with enableUrl if the person has not turned this tool on for connectors. Tell them once and do not call it again until they say it is on.
{ "type": "object", "required": [ "blog" ], "properties": { "blog": { "type": "string", "description": "The blog id or any link to the blog (blog.naver.com/<id>, m.blog.naver.com/<id>/<logNo>, PostView.naver?blogId=<id>)." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linesexpand_naver_keywords reads auth-required 42d ago
Naver Blog only. Expands seed keywords one level through Naver autocomplete and returns every word found with the seed it came from (found[].from is seed or L1:<seed>). These are candidates to measure with research_naver_keywords, not proven keywords. The person's uplika Chrome extension looks them up in the background when it is on (no window opens); otherwise our server does, and via says which path answered each seed. Up to 10 seeds. Cached seven days. There is no daily cap: the extension path has no wait, and when our server answers, each person gets one expansion every 60 seconds (naver_autocomplete_busy says how long to wait). When the answer is naver_autocomplete_unavailable, neither path could run: pass your own keyword list to research_naver_keywords instead. naver_autocomplete_paused and naver_autocomplete_busy carry retryAfterSeconds. Returns 403 research_tool_disabled with enableUrl if the person has not turned this tool on for connectors. Tell them once and do not call it again until they say it is on.
{ "type": "object", "required": [ "seeds" ], "properties": { "seeds": { "type": "array", "items": { "type": "string" }, "description": "1-10 seed keywords." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 19 linesget_naver_keyword_history reads auth-required 42d ago
The measurement history of one keyword: one point per day it was actually measured, newest first, with search volume, document count and posts per month. Shows whether a keyword is rising or cooling. An empty list means nobody has measured it here yet. Returns 403 research_tool_disabled with enableUrl if the person has not turned this tool on for connectors. Tell them once and do not call it again until they say it is on.
{ "type": "object", "required": [ "keyword" ], "properties": { "limit": { "type": "number", "description": "How many days, default 100." }, "keyword": { "type": "string", "description": "The keyword, as you would send it to research_naver_keywords." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 20 linesget_quota reads auth-required 46d ago
How much of the 24 hour allowance is already used for posts, replies and deletes. Check this before a burst of publishing. This is live usage from the platform, not the static limits in list_platforms. We also cap how fast one account can publish, so publish can return rate_limited even when the platform allowance still has room.
{ "type": "object", "properties": { "accountId": { "type": "string", "description": "Limit to one account. Omit to cover every connected channel." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 13 linesmedia_presign auth-required never probed
Use this only when you can HTTP PUT the file bytes yourself (a shell or code runtime). Web and mobile chat clients cannot, so do not call it there: for a file on the person's device use media_upload_link, for a public https address use media_from_url. Returns a media id and a one-time uploadUrl. PUT the file bytes to uploadUrl with the same contentType, then call media_complete. Images: image/jpeg, image/png, image/webp, image/gif, up to 20MB. Video: video/mp4, video/quicktime, video/webm, up to 8GB, 43200 seconds, 4096px wide. Aspect ratio up to 20:1. Any image pixel width is fine. Documents for DM automations only (deliver.mediaId), not for posts: application/pdf, application/x-hwp, application/hwp+zip (.pdf, .hwp, .hwpx), up to 25MB. If you can see the image, write altText describing it. The response has mediaExpiresAt: this media id disappears after that time if it was never published, and publish will then fail with media_expired.
{ "type": "object", "required": [ "fileName", "contentType", "bytes" ], "properties": { "bytes": { "type": "number", "description": "File size in bytes" }, "width": { "type": "number", "description": "Pixel width, if you know it" }, "height": { "type": "number", "description": "Pixel height, if you know it" }, "altText": { "type": "string", "description": "What the image shows, for people using screen readers. Write it whenever you can see the image. Applied to carousel items." }, "fileName": { "type": "string" }, "aiGenerated": { "type": "boolean", "description": "true when the image or video was generated or substantially changed with AI. Naver Blog photos get its AI-use label; Instagram, YouTube and TikTok get their AI declaration turned on unless you set it to false. Leave it out when unsure." }, "contentType": { "type": "string", "description": "image/jpeg or image/png or image/webp or image/gif or video/mp4 or video/quicktime or video/webm or application/pdf or application/x-hwp or application/hwp+zip" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 41 linesupdate_media auth-required never probed
Mark or unmark an uploaded image or video as AI-made (aiGenerated). Applies to posts published or edited after this call; posts already out do not change (use update_post to rewrite a Naver Blog post). Every copy of the same file in your workspaces follows.
{ "type": "object", "required": [ "id", "aiGenerated" ], "properties": { "id": { "type": "string", "description": "Media id from media_presign, media_from_url or media_upload_status." }, "aiGenerated": { "type": "boolean", "description": "true to mark as AI-made, false to clear." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 21 linesmedia_complete auth-required never probed
Step 2 of attaching an image or video. Call it after the upload finishes. We check the file really landed before marking it ready. Only a ready media id can be passed to publish.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "Media id from media_presign" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 lineslist_automations reads auth-required 45d ago
Automations on the connected accounts: name, channel, whether it is live or a draft, triggers, run count, and templateParams when the flow still has its template shape. Filter by accountId, status (draft or live) or templateId. Use get_automation to read one flow's document.
{ "type": "object", "properties": { "status": { "enum": [ "draft", "live" ], "type": "string", "description": "draft = not enabled, live = enabled." }, "accountId": { "type": "string", "description": "Only automations on this connected account." }, "templateId": { "type": "string", "description": "Only automations made from this template (list_automation_templates)." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 25 linesget_automation auth-required 42d ago
One automation as a document: triggers, nodes, start. Also returns version, which put_automation and update_automation need, and templateParams when the flow still has its template shape. pastComments is the latest send_to_past_comments job with its progress, or null. Pass version to read an older saved version's document instead (list_automation_versions).
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "Automation id from list_automations." }, "version": { "type": "integer", "description": "A saved version number from list_automation_versions. Leave out for the current one." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 20 linescreate_comment_to_dm auth-required never probed
The most common automation: when someone comments on a post, DM them. **Creates a draft; nothing goes out until enable_automation.** How Meta works: a DM cannot be started by the account. The only automatic door is a private reply to a comment, one per comment, within 7 days of the comment, and once the person answers the 24-hour window opens for the rest. Instagram can check whether the person follows the account (requireFollow); Facebook cannot, so requireFollow is rejected there. Threads has no DMs at all; use create_automation with comment_public_reply. A file to deliver: if it already has a public https link that returns the file itself (jpeg, png, gif, webp, mp4, mov or PDF), pass it as deliver.fileUrl and it goes to the person as-is; the link is opened once on save and a web page (a Google Drive or Dropbox share page), a private address or a dead link is rejected. If the file has no public link, upload it to Uplika first (media_upload_link without a shell, media_presign then media_complete with one) and pass deliver.mediaId (photos, videos, PDF or HWP/HWPX documents; on Instagram a document other than PDF goes as a download link, because Instagram DMs attach PDF only). Not both. post can be our post id, the post's own id on the platform, a link to the post, "any" for every post, or "next" for the next post you publish (or pass automation on publish to do both in one call). Only one enabled automation per account can wait for "next" (409 next_post_taken). Order when everything is on: opening DM (message + button) -> askEmail -> requireFollow -> deliver (text, up to three link buttons, file; clicks are tracked) -> followUp if no link was clicked. openingDm:false sends deliver as the private reply itself; then requireFollow, askEmail, followUp and files are rejected because the window never opens. The optional public reply under the comment is fixed sentences (publicReply) or written by the AI for each comment (publicReplyMode: "ai" with publicReplyInstruction); either way it is posted only after the private reply went out, and the AI is told a DM was sent.
{ "type": "object", "required": [ "accountId", "post", "deliver" ], "properties": { "name": { "type": "string" }, "post": { "type": "string", "description": "Our post id, the platform post id, a link to the post, \"any\", or \"next\"." }, "match": { "enum": [ "contains", "is", "whole_word", "begins_with", "thumbs_up", "not_contains" ], "type": "string" }, "deliver": { "type": "object", "properties": { "link": { "type": "string", "description": "Legacy: one url appended to the text. Prefer links." }, "text": { "type": "string" }, "links": { "type": "array", "items": { "type": "object", "required": [ "title", "url" ], "properties": { "url": { "type": "string" }, "title": { "type": "string", "description": "At most 20 characters." } } }, "maxItems": 3 }, "fileUrl": { "type": "string", "description": "Public https link to the file itself, sent as-is. Checked once on save." }, "mediaId": { "type": "string", "description": "A file uploaded to Uplika media, for a file with no public link." }, "mediaKind": { "enum": [ "file", "image", "video" ], "type": "string", "description": "Filled in from the file when you pass fileUrl." } }, "description": "What to send after the tap: text, up to three link buttons, and/or one file (fileUrl or mediaId)." }, "enabled": { "type": "boolean", "description": "Default false. Prefer leaving it off and calling enable_automation after the person confirms." }, "message": { "type": "string", "description": "The opening DM (private reply). One message. Required unless openingDm is false." }, "askEmail": { "type": "boolean", "description": "Ask for their email after the tap and store it in contact field `email`. Three tries, then continue without." }, "followUp": { "type": "string", "description": "Sent followUpAfterMinutes later if none of deliver.links was clicked. Needs openingDm and at least one link." }, "keywords": { "type": "array", "items": { "type": "string" }, "description": "Trigger words in the comment. Empty means every comment." }, "accountId": { "type": "string", "description": "Connected Instagram or Facebook account id from list_accounts." }, "openingDm": { "type": "boolean", "description": "Default true. false: deliver goes out as the private reply itself (no button, no window afterwards)." }, "emailRetry": { "type": "string" }, "buttonTitle": { "type": "string", "description": "Button under the opening DM, at most 20 characters." }, "likeComment": { "type": "boolean", "description": "Like the comment first. Instagram accounts connected through Facebook only." }, "publicReply": { "type": "array", "items": { "type": "string" }, "description": "Optional public replies under the comment, one picked at random. Posted after the private reply goes out, and skipped when it could not be sent, so a reply saying a DM was sent stays true." }, "workspaceId": { "type": "string", "description": "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." }, "emailMessage": { "type": "string" }, "recheckTitle": { "type": "string" }, "requireFollow": { "type": "boolean", "description": "Instagram only. Deliver only to followers; others are asked to follow and check again." }, "publicReplyMode": { "enum": [ "fixed", "ai" ], "type": "string", "description": "fixed (default) uses publicReply. ai: the AI writes the public reply for each comment in the channel's persona; needs publicReplyInstruction. Counts toward the daily AI limit." }, "notFollowingMessage": { "type": "string" }, "followUpAfterMinutes": { "type": "number", "description": "1 to 1380 (23 hours). Default 60." }, "publicReplyInstruction": { "type": "string", "description": "With publicReplyMode ai: what the public reply should say. DM contents (links, codes, prices) are never repeated in public." } } }arguments 160 linescreate_automation auth-required never probed
Create an automation from any template in list_automation_templates by passing its params. Creates a draft. For a flow no template covers, build the document yourself and call put_automation on the draft.
{ "type": "object", "required": [ "accountId", "templateId", "params" ], "properties": { "name": { "type": "string" }, "params": { "type": "object", "description": "The template's params." }, "enabled": { "type": "boolean", "description": "Default false." }, "accountId": { "type": "string", "description": "Connected account id from list_accounts." }, "templateId": { "type": "string", "description": "Template id from list_automation_templates." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 33 linesput_automation auth-required never probed
Edit an automation by replacing its whole document: for flows no template covers, or that were already edited on the canvas. If get_automation shows templateParams, change it with update_automation instead (only the params you pass change; an Instagram follow check is requireFollow, notFollowingMessage and recheckTitle). Replacing the document of a flow that still has its template form is refused with automation_is_template, because the person can no longer open it in the form afterwards; pass detachTemplate: true only when the change cannot be expressed as template params. Read it first with get_automation and pass the version you got; a stale version is refused with version_conflict so a concurrent edit is not overwritten. Node types: send (mode window | private_reply | public_reply), condition, action, delay, random, goto, ai. Waiting is a send node with buttons and next: null; the tapped button's next continues. A loop must pass through such a wait. A comment trigger's post.postId takes the post's link too; the server resolves it to the uplika post id. Validation problems come back with paths. Changing a live flow changes what goes out to people.
{ "type": "object", "required": [ "id", "version", "doc" ], "properties": { "id": { "type": "string" }, "doc": { "type": "object", "required": [ "v", "triggers", "start", "nodes" ], "properties": { "v": { "type": "integer", "const": 1 }, "nodes": { "type": "object", "additionalProperties": { "type": "object", "required": [ "type" ], "properties": { "all": { "type": "array", "items": { "type": "object", "required": [ "field", "op" ], "properties": { "op": { "enum": [ "eq", "ne", "gt", "lt", "has", "not_has", "contains" ], "type": "string" }, "key": { "type": "string" }, "field": { "enum": [ "tag", "follows_us", "we_follow", "follower_count", "verified", "window_open", "opted_in", "last_interaction_hours", "field", "intent", "sentiment", "text", "clicked" ], "type": "string" }, "value": {} } } }, "else": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "flow": { "type": "string" }, "mode": { "enum": [ "window", "private_reply", "public_reply", "post_comment" ], "type": "string", "description": "send: window = inside the 24-hour window, private_reply = one DM to a commenter, public_reply = comment under the post. ai: window | public_reply | post_comment (post_comment = read the neighbor's post that triggered the flow and leave one comment; neighbor_post trigger only)." }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "then": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "type": { "enum": [ "send", "condition", "action", "delay", "random", "goto", "ai", "ask" ], "type": "string" }, "field": { "type": "string", "description": "ask: contact field the answer is stored in (e.g. email)." }, "items": { "type": "array", "items": { "type": "object", "required": [ "type" ], "properties": { "key": { "type": "string" }, "tag": { "type": "string" }, "type": { "enum": [ "add_tag", "remove_tag", "set_field", "clear_field", "opt_in", "opt_out", "open_conversation", "close_conversation", "assign", "hide_reply", "approve_reply", "follow", "like_comment" ], "type": "string" }, "value": { "type": "string" }, "mutual": { "type": "boolean", "description": "follow: ask for a mutual neighbor relation (Naver). Default false." }, "userId": { "type": "string" }, "message": { "type": "string", "description": "follow: message on the mutual request." } } } }, "retry": { "type": "string" }, "blocks": { "type": "array", "items": { "type": "object", "required": [ "type" ], "properties": { "url": { "type": "string" }, "text": { "type": "string" }, "type": { "enum": [ "text", "image", "video", "audio", "file", "delay", "card", "gallery" ], "type": "string" }, "cards": { "type": "array", "items": { "type": "object" } }, "title": { "type": "string" }, "buttons": { "type": "array", "items": { "type": "object", "required": [ "id", "title", "kind" ], "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "kind": { "enum": [ "postback", "url" ], "type": "string" }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "title": { "type": "string", "description": "At most 20 characters." } } } }, "mediaId": { "type": "string" }, "seconds": { "type": "number" }, "imageUrl": { "type": "string" }, "subtitle": { "type": "string" } } } }, "buttons": { "type": "array", "items": { "type": "object", "required": [ "id", "title", "kind" ], "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "kind": { "enum": [ "postback", "url" ], "type": "string" }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "title": { "type": "string", "description": "At most 20 characters." } } }, "maxItems": 3 }, "seconds": { "type": "number" }, "branches": { "type": "array", "items": { "type": "object", "required": [ "weight" ], "properties": { "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "weight": { "type": "number" } } } }, "escalate": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "maxTries": { "type": "integer", "description": "ask: default 3. After that the flow continues without a value." }, "maxTurns": { "type": "integer" }, "validate": { "enum": [ "email", "none" ], "type": "string", "description": "ask: how to check the answer. Wrong answers get `retry`, up to maxTries." }, "usePersona": { "type": "boolean" }, "instruction": { "type": "string" }, "quickReplies": { "type": "array", "items": { "type": "object", "required": [ "id", "title" ], "properties": { "id": { "type": "string" }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "title": { "type": "string" } } }, "maxItems": 13 } } } }, "start": { "type": [ "string", "null" ] }, "layout": { "type": "object", "additionalProperties": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } } } }, "template": { "type": "object", "properties": { "id": { "type": "string" }, "params": { "type": "object" } } }, "triggers": { "type": "array", "items": { "type": "object", "required": [ "kind" ], "properties": { "at": { "type": "string" }, "ref": { "type": "string" }, "tag": { "type": "string" }, "kind": { "enum": [ "comment", "live_comment", "message", "story_reply", "story_mention", "referral", "mention", "neighbor_post", "contact_created", "tag_added", "tag_removed", "field_changed", "schedule" ], "type": "string" }, "post": { "oneOf": [ { "enum": [ "any", "next" ], "type": "string" }, { "type": "object", "required": [ "postId" ], "properties": { "postId": { "type": "string" } } } ], "description": "comment triggers: { postId } | \"any\" | \"next\". postId may also be the post's link (the server resolves it to the uplika post id when saving). \"next\" binds to the next post you publish." }, "field": { "type": "string" }, "keywords": { "type": [ "object", "null" ], "required": [ "match", "values" ], "properties": { "match": { "enum": [ "is", "contains", "whole_word", "begins_with", "thumbs_up", "not_contains" ], "type": "string" }, "values": { "type": "array", "items": { "type": "string" } } } }, "neighbors": { "enum": [ "mutual", "all" ], "type": "string", "description": "neighbor_post trigger (Naver Blog): mutual = only mutual neighbors (default), all = every neighbor in the feed." }, "publicReply": { "type": "array", "items": { "type": "string" }, "description": "Also reply publicly under the comment. Several entries are picked at random." }, "publicReplyAi": { "type": "object", "required": [ "instruction" ], "properties": { "instruction": { "type": "string" } }, "description": "Instead of publicReply: the AI writes the public reply for each comment, after the flow ran. Told a DM was sent only when the private reply went out." } } }, "minItems": 1 } }, "description": "One automation flow. triggers start it, nodes are the steps, start names the first node. Waiting is not a node: a send node with buttons and next: null waits for the person to tap one, and that button's next continues. A loop must pass through such a wait." }, "name": { "type": "string" }, "version": { "type": "integer" }, "workspaceId": { "type": "string", "description": "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." }, "detachTemplate": { "type": "boolean", "description": "Only for a flow that still has its template form: true turns it into a canvas-only flow the form can no longer open. Default false." } } }arguments 559 linesenable_automation auth-required never probed
Turn an automation live. **This is the moment messages start going to real people.** Confirm with the person first. Only one automation per account can wait for the next post: enabling a second one is refused with next_post_taken until the first binds to a post. Only one message flow without keywords (a default reply or AI replies) can be live per account, or one message would get two answers: enabling a second is refused with automation_catch_all_taken, which names the live one. Refuses with reconsent_required if the account does not hold the DM permissions (the person reconnects it), with feature_in_review if the flow uses a feature whose permission is still in Meta app review (a mention trigger, liking a comment on Instagram) and the account does not already hold it, and warns if the account is not subscribed to webhooks.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 15 linesduplicate_automation auth-required never probed
Copy an automation as a new draft with the same triggers and settings, optionally with a new name. Runs and versions are not copied; a copy bound to a specific post keeps that post, a copy of a next-post flow waits for the next post again when enabled.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "name": { "type": "string", "description": "Default: the original name with (copy)." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 19 linesvalidate_automation auth-required never probed
Check a flow document against the rules for one account without saving it: node shapes, references, the 24-hour window, and features still in Meta review. Answers problems with paths; an empty list means put_automation would accept it.
{ "type": "object", "required": [ "accountId", "doc" ], "properties": { "doc": { "type": "object", "required": [ "v", "triggers", "start", "nodes" ], "properties": { "v": { "type": "integer", "const": 1 }, "nodes": { "type": "object", "additionalProperties": { "type": "object", "required": [ "type" ], "properties": { "all": { "type": "array", "items": { "type": "object", "required": [ "field", "op" ], "properties": { "op": { "enum": [ "eq", "ne", "gt", "lt", "has", "not_has", "contains" ], "type": "string" }, "key": { "type": "string" }, "field": { "enum": [ "tag", "follows_us", "we_follow", "follower_count", "verified", "window_open", "opted_in", "last_interaction_hours", "field", "intent", "sentiment", "text", "clicked" ], "type": "string" }, "value": {} } } }, "else": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "flow": { "type": "string" }, "mode": { "enum": [ "window", "private_reply", "public_reply", "post_comment" ], "type": "string", "description": "send: window = inside the 24-hour window, private_reply = one DM to a commenter, public_reply = comment under the post. ai: window | public_reply | post_comment (post_comment = read the neighbor's post that triggered the flow and leave one comment; neighbor_post trigger only)." }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "then": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "type": { "enum": [ "send", "condition", "action", "delay", "random", "goto", "ai", "ask" ], "type": "string" }, "field": { "type": "string", "description": "ask: contact field the answer is stored in (e.g. email)." }, "items": { "type": "array", "items": { "type": "object", "required": [ "type" ], "properties": { "key": { "type": "string" }, "tag": { "type": "string" }, "type": { "enum": [ "add_tag", "remove_tag", "set_field", "clear_field", "opt_in", "opt_out", "open_conversation", "close_conversation", "assign", "hide_reply", "approve_reply", "follow", "like_comment" ], "type": "string" }, "value": { "type": "string" }, "mutual": { "type": "boolean", "description": "follow: ask for a mutual neighbor relation (Naver). Default false." }, "userId": { "type": "string" }, "message": { "type": "string", "description": "follow: message on the mutual request." } } } }, "retry": { "type": "string" }, "blocks": { "type": "array", "items": { "type": "object", "required": [ "type" ], "properties": { "url": { "type": "string" }, "text": { "type": "string" }, "type": { "enum": [ "text", "image", "video", "audio", "file", "delay", "card", "gallery" ], "type": "string" }, "cards": { "type": "array", "items": { "type": "object" } }, "title": { "type": "string" }, "buttons": { "type": "array", "items": { "type": "object", "required": [ "id", "title", "kind" ], "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "kind": { "enum": [ "postback", "url" ], "type": "string" }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "title": { "type": "string", "description": "At most 20 characters." } } } }, "mediaId": { "type": "string" }, "seconds": { "type": "number" }, "imageUrl": { "type": "string" }, "subtitle": { "type": "string" } } } }, "buttons": { "type": "array", "items": { "type": "object", "required": [ "id", "title", "kind" ], "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "kind": { "enum": [ "postback", "url" ], "type": "string" }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "title": { "type": "string", "description": "At most 20 characters." } } }, "maxItems": 3 }, "seconds": { "type": "number" }, "branches": { "type": "array", "items": { "type": "object", "required": [ "weight" ], "properties": { "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "weight": { "type": "number" } } } }, "escalate": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "maxTries": { "type": "integer", "description": "ask: default 3. After that the flow continues without a value." }, "maxTurns": { "type": "integer" }, "validate": { "enum": [ "email", "none" ], "type": "string", "description": "ask: how to check the answer. Wrong answers get `retry`, up to maxTries." }, "usePersona": { "type": "boolean" }, "instruction": { "type": "string" }, "quickReplies": { "type": "array", "items": { "type": "object", "required": [ "id", "title" ], "properties": { "id": { "type": "string" }, "next": { "type": [ "string", "null" ], "description": "Id of the next node, or null to stop." }, "title": { "type": "string" } } }, "maxItems": 13 } } } }, "start": { "type": [ "string", "null" ] }, "layout": { "type": "object", "additionalProperties": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } } } }, "template": { "type": "object", "properties": { "id": { "type": "string" }, "params": { "type": "object" } } }, "triggers": { "type": "array", "items": { "type": "object", "required": [ "kind" ], "properties": { "at": { "type": "string" }, "ref": { "type": "string" }, "tag": { "type": "string" }, "kind": { "enum": [ "comment", "live_comment", "message", "story_reply", "story_mention", "referral", "mention", "neighbor_post", "contact_created", "tag_added", "tag_removed", "field_changed", "schedule" ], "type": "string" }, "post": { "oneOf": [ { "enum": [ "any", "next" ], "type": "string" }, { "type": "object", "required": [ "postId" ], "properties": { "postId": { "type": "string" } } } ], "description": "comment triggers: { postId } | \"any\" | \"next\". postId may also be the post's link (the server resolves it to the uplika post id when saving). \"next\" binds to the next post you publish." }, "field": { "type": "string" }, "keywords": { "type": [ "object", "null" ], "required": [ "match", "values" ], "properties": { "match": { "enum": [ "is", "contains", "whole_word", "begins_with", "thumbs_up", "not_contains" ], "type": "string" }, "values": { "type": "array", "items": { "type": "string" } } } }, "neighbors": { "enum": [ "mutual", "all" ], "type": "string", "description": "neighbor_post trigger (Naver Blog): mutual = only mutual neighbors (default), all = every neighbor in the feed." }, "publicReply": { "type": "array", "items": { "type": "string" }, "description": "Also reply publicly under the comment. Several entries are picked at random." }, "publicReplyAi": { "type": "object", "required": [ "instruction" ], "properties": { "instruction": { "type": "string" } }, "description": "Instead of publicReply: the AI writes the public reply for each comment, after the flow ran. Told a DM was sent only when the private reply went out." } } }, "minItems": 1 } }, "description": "One automation flow. triggers start it, nodes are the steps, start names the first node. Waiting is not a node: a send node with buttons and next: null waits for the person to tap one, and that button's next continues. A loop must pass through such a wait." }, "accountId": { "type": "string", "description": "The connected account the flow would run on." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 549 lineslist_automation_runs reads auth-required never probed
Runs of one automation with their status and a step log. Status says where a run is or why it stopped: running, waiting, done, superseded, blocked_window (24-hour window closed), blocked_opt_out, blocked_paused (a person is handling that conversation), blocked_burst, blocked_ai_quota (the workspace used its daily automation AI limit; it resets at 00:00 UTC), failed_channel, failed_ai (the AI step could not classify the comment or write the reply; the reason is in the log as classify_failed or ai_failed), expired.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "limit": { "type": "integer" }, "before": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 21 linesread_conversation reads auth-required 42d ago
One conversation with its recent messages and the contact: name, whether they follow the account (Instagram only), tags, opt-out. AI drafts waiting for approval show as status draft.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 15 linessend_dm auth-required never probed
Send a message in a conversation as the account. **Only inside the 24-hour window** after the person's last message; outside it the call is refused with window_closed and nothing can be done until they write again. On a mention thread (Threads) this posts a public reply. Pass draftId to send an AI draft that was waiting for approval. Sending pauses automations on that conversation for 30 minutes.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "Conversation id from list_conversations." }, "text": { "type": "string" }, "draftId": { "type": "string", "description": "Message id of an AI draft to send instead of new text." }, "mediaId": { "type": "string", "description": "Optional media id from media_presign." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 27 lineslist_mentions auth-required 45d ago
Threads posts that mention the connected account, delivered by webhook. Same shape as list_conversations with kind mention. Reply with send_dm, which posts publicly. Mentions need a permission of their own: when no connected Threads account holds it the list is empty and the response carries notice (code feature_in_review while that permission is in Meta app review, reconsent_required once reconnecting grants it). list_platforms shows the state.
{ "type": "object", "properties": { "unread": { "type": "boolean" }, "accountId": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 15 linesselect_channels changes data auth-required never probed
Pick which connected channels to post to. **Call this before publish and show the result to the person.** Leave scope empty to get the candidate list and let them choose. Use scope: "all" for every active channel, or an array mixing platform names ("threads"), handles ("@vibe.trender") and account ids. Duplicates are folded, expired and not-yet-live channels are dropped into `skipped` with a reason, a sentence you can read to the person, and a link to reconnect. Pass the returned accountIds to publish unchanged. `limits` is the tightest rule across the chosen channels, so write to that. If both accountIds and candidates come back empty, nothing is connected yet. Send the person to https://uplika.com/dashboard/connections to connect a channel, then call this again. When a Naver Blog account is selected the response carries bridge with state (online, offline, logged_out, login_needed), userMessage to relay to the person, and queued; if state is not online, tell the person before publishing.
{ "type": "object", "properties": { "scope": { "anyOf": [ { "enum": [ "all" ], "type": "string" }, { "type": "array", "items": { "type": "string" } } ], "description": "Omit to list candidates, \"all\" for every active channel, or an array of platform names, handles or account ids." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 26 lineslist_platforms auth-required 44d ago
Every channel and its rules: character limit, whether media is required, image and video limits, and what state the channel is in. Read this instead of guessing a platform's limits. status says who can connect: live means anyone; beta means the channel is in platform review and only accounts registered as testers on our app can connect yet, though publishing works normally for those accounts; bridge means it needs the user's browser extension running; soon means it is not connectable at all. charCount tells you how that channel counts a character, so you can check the length before calling publish instead of after it fails. options is the JSON Schema of what options.<channel> takes on publish: which fields exist, which are required, and the allowed values. Read it instead of guessing a channel's settings.
{ "type": "object", "properties": { "workspaceId": { "type": "string", "description": "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." } } }arguments 9 linesget_help reads auth-required 39d ago
Uplika's troubleshooting answers: why something did not work and what the person can do about it. Call it with the error code you got (code), or with a short question in any language (query), when a call fails, when an automation did not react or a DM did not arrive, or when the person asks why something happened. Without arguments it lists every question with its id; pass id for one answer in full. Each answer has the steps, links and a url to the same answer on uplika.com that you can give the person.
{ "type": "object", "properties": { "id": { "type": "string", "description": "One answer's id from the list, e.g. thread-control." }, "code": { "type": "string", "description": "An error code from an Uplika response, e.g. reconsent_required or window_closed." }, "query": { "type": "string", "description": "A short question or a few words in any language, e.g. \"DM not sent after button\" or \"ManyChat\"." }, "locale": { "enum": [ "en", "ko" ], "type": "string", "description": "Language of the answer. en (default) or ko; use the person's language." }, "platform": { "type": "string", "description": "Only answers about this platform id (plus general ones), e.g. instagram." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 33 linesopen_post auth-required never probed
Everything about one post in a single call: the text, the whole reply thread, and its metrics. This is the right tool when someone hands you a post link. Replies or metrics can come back null if the platform refused just that part. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linespublish_now auth-required never probed
Send a scheduled or draft post right now instead of waiting. Returns while it is still publishing, like publish; pass wait: true to hold for the result. A draft needs at least one target account first. Posts that already went out return post_not_editable.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "wait": { "type": "boolean", "description": "Hold the response until the post is really out, same as on publish." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 20 lineslist_replies reads auth-required 42d ago
The whole reply thread under a post, nested replies included. `truncated` tells you we stopped before the end. The count here can differ from the replies metric in get_insights, which is normal. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linesreply sends messages auth-required never probed
Reply to a post or to a reply. Leave replyTo empty to reply to the post itself; pass a reply id from list_replies to nest a reply under that reply. Like publish, this returns before the reply is live unless you pass wait: true, which holds the response until it is out (up to 10 seconds). Otherwise call get_post with the returned id to see the final status and link. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id", "content" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "wait": { "type": "boolean", "description": "Hold the response until the reply is really out, up to 10 seconds, so you can say it is live. If it is still going you get the usual publishing response; poll get_post. Defaults to false." }, "secret": { "type": "boolean", "description": "Naver Blog only: post it as a secret comment that only the blog owner can read. Other channels refuse it." }, "content": { "type": "string" }, "replyTo": { "type": "string", "description": "Reply id from list_replies. Omit to reply to the post itself." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 32 linesdelete_reply changes data auth-required never probed
Delete a comment for good. This is not hide_reply: it cannot be undone. What it reaches differs by channel and list_platforms says which ones support it at all. On Instagram and Facebook it removes anyone's comment on your post; on Threads and Bluesky a reply is itself a post, so it only removes replies the connected account wrote. Prefer hide_reply when the person just wants it out of sight.
{ "type": "object", "required": [ "replyId", "postId" ], "properties": { "postId": { "type": "string", "description": "The post the reply sits under. A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "replyId": { "type": "string", "description": "Reply id from list_replies" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 21 linesget_insights auth-required 42d ago
Views, likes, replies, reposts, quotes and shares for one publish. Views and shares can be null when the platform does not report them yet — null is not zero. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linesbridge_status reads auth-required 46d ago
Naver Blog only. Posts to Naver Blog are written by the uplika browser extension inside the user's own Chrome, so nothing goes out while that Chrome is closed. Call this before publishing to Naver Blog. If online is false, the response carries wake commands per OS that open Chrome on the user's computer in the profile that has the extension (found by extension id), for the person to run. Once Chrome is open the extension reconnects within about a minute and queued posts go out; call this again or get_post to check. publish also returns the same bridge object when the extension is offline. state is one of online, offline, logged_out (Chrome is on but not logged in to Naver), login_needed (the Naver login saved for that blog in that Chrome is signed out; its posts wait until the person logs in again from the dashboard). userMessage is a sentence in the person's language to relay as it is. queued is how many posts wait for the extension; delete_post cancels them and update_post rewrites them before they go out. extensionVersion and kinds say what that extension can do.
{ "type": "object", "properties": { "accountId": { "type": "string", "description": "Limit to one Naver Blog account. Omit to cover every connected Naver Blog account." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 13 linespublish_naver_draft sends messages auth-required never probed
Naver Blog only. Publishes a draft from the blog's draft box exactly as it is in Naver: the extension loads that draft in the editor and presses publish, so edits the person made by hand in Naver are kept. Do not send content. Category, tags and openType are taken from the draft unless you pass them. Returns the same shape as publish (202 with a target that resolves to published, or the bridge object when the extension is offline; wait: true holds until it settles). If that draft was made through uplika (a target whose externalId starts with draft:), the same post record flips to published instead of a second one appearing. To publish uplika's own copy of the text rather than what is in Naver, use update_post on that post instead. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "logNo" ], "properties": { "tags": { "type": "array", "items": { "type": "string" }, "description": "Replace the draft's tags (no #)." }, "wait": { "type": "boolean", "description": "Hold until the extension finishes, up to 75 seconds. Past that it returns while still going out; read it with get_post." }, "logNo": { "type": "string", "description": "The draft's logNo from list_naver_drafts." }, "title": { "type": "string", "description": "Replace the draft's title." }, "category": { "type": "string", "description": "Category by name instead of id." }, "openType": { "enum": [ "public", "neighbor", "mutual", "private" ], "type": "string", "description": "Visibility. Default: the draft's own setting." }, "accountId": { "type": "string", "description": "Which Naver Blog account. Required only when the workspace has more than one." }, "categoryId": { "type": "string", "description": "Category id from list_accounts. Default: the draft's own." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 53 linesresearch_naver_keywords auth-required never probed
Naver Blog only. Measures keywords (monthly searches from Search Ad, blog document count and posts per month from API HUB), judges each one (best, possible, hard, wall, hot, saturated, phantom and so on, with why), and groups them into sets for one post: a main keyword plus two to five subs with the same search intent. Every set carries prompt, a ready-to-paste Korean brief for writing the skeleton of that post. Up to 60 keywords. Measurements are cached seven days across users; new ones run against a time budget, so partial: true with unmeasured[] means the budget ran out and calling again with the same keywords finishes the rest from cache. Each call is saved as a report (reportId) unless the same keyword set was saved in the last ten minutes, which returns that report's id instead. Every set also carries draftPrompt, a brief for writing the whole post (title, subheadings, body, and bracketed photo placeholders for the person to fill); pass topic to put the post's subject in it. Returns 403 research_tool_disabled with enableUrl if the person has not turned this tool on for connectors. Tell them once and do not call it again until they say it is on.
{ "type": "object", "required": [ "keywords" ], "properties": { "topic": { "type": "string", "description": "What the post is about, one line. Goes into draftPrompt; without it the prompt tells the writer to pick a subject that fits the main keyword." }, "keywords": { "type": "array", "items": { "type": "string" }, "description": "1-60 keywords, as the person would search them." }, "seedText": { "type": "string", "description": "What the person asked for, kept as the report's label. Defaults to the first five keywords." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 27 linesmedia_from_url changes data auth-required never probed
Attach an image or video that is already on the public web. We download it, copy it into our storage and give you a media id you can pass to publish. One step, no upload needed. https only. Google Drive and Dropbox **share** links do not work: they return an HTML preview page, not the file. Use a direct file URL that ends in the file itself. Accepted formats (read from the bytes, not the headers): image/jpeg, image/png, image/gif, image/webp, video/mp4, video/quicktime. Each channel then checks what it takes when you publish. If you can see the image, write altText describing it.
{ "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "description": "Public https URL of the image or video file itself." }, "altText": { "type": "string", "description": "What the image shows, for people using screen readers. Applied to carousel items." }, "aiGenerated": { "type": "boolean", "description": "true when the image or video was generated or substantially changed with AI. Naver Blog photos get its AI-use label; Instagram, YouTube and TikTok get their AI declaration turned on unless you set it to false. Leave it out when unsure." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 24 linesmedia_upload_status reads auth-required never probed
Has the person uploaded yet? Returns waiting, ready or expired, plus every media id uploaded through that link. Pass those ids to publish as mediaIds. Ready images also come back as image blocks so you can SEE each photo and place it in the right paragraph: previewIds[i] is the media id of the i-th image. Up to 20 images per call; pass offset to see the rest. A media with duplicateOf is the same bytes as that other id — use one of them.
{ "type": "object", "required": [ "token" ], "properties": { "token": { "type": "string", "description": "The token from media_upload_link." }, "offset": { "type": "integer", "description": "Skip this many images before returning previews (default 0)." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 20 lineslist_automation_templates auth-required 45d ago
Ready-made automation templates with the params each one takes. Read this before create_automation. Every template lists the channels it works on. The most used one is comment_to_dm: a comment on a post gets one private reply with a button, and tapping it delivers a link or file inside the messaging window. AI-written comment replies are comment_public_reply with replyMode "ai" (and comment_to_dm's public reply with publicReplyMode "ai"); the old template id ai_comment_reply still works and the answer says renamedFrom.
{ "type": "object", "properties": { "workspaceId": { "type": "string", "description": "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." } } }arguments 9 linesdisable_automation changes data auth-required never probed
Stop an automation. Runs already waiting for a button stay waiting but nothing new starts.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 15 linesupdate_automation changes data auth-required never probed
Edit an automation that still has its template shape, without rebuilding the whole document: pass only the template params you want to change (the same params as create_automation / create_comment_to_dm). Top-level fields are replaced, object fields such as deliver merge one level deep (deliver.text alone keeps deliver.links), null removes a field, arrays are replaced whole. Read it first with get_automation and pass its version; a stale version answers version_conflict. Also renames (name) and turns it on or off (enabled, with the same checks as enable_automation). Instagram follow check: requireFollow, notFollowingMessage, recheckTitle. A flow that was edited on the canvas has no template shape and answers automation_not_template: use put_automation for it. Changing a live flow changes what goes out to people right away.
{ "type": "object", "required": [ "id", "version" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "params": { "type": "object", "description": "Template params to change. Only what you pass changes." }, "enabled": { "type": "boolean" }, "version": { "type": "integer", "description": "The version from get_automation or list_automations." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 30 linesdelete_automation changes data auth-required never probed
Delete an automation and its run history. Cannot be undone. A live automation is refused with automation_live: disable it first, or pass force: true after the person confirms, because deleting it stops what is going out to people.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "force": { "type": "boolean", "description": "Delete even if it is live. Default false." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 19 lineslist_conversations reads auth-required 45d ago
The inbox: DM conversations on Instagram and Facebook and mention threads on Threads. Each item says whether the 24-hour window is open and how long is left. state: open (default) or closed. kind: dm or mention.
{ "type": "object", "properties": { "kind": { "enum": [ "dm", "mention" ], "type": "string" }, "limit": { "type": "integer" }, "state": { "enum": [ "open", "closed" ], "type": "string" }, "search": { "type": "string" }, "unread": { "type": "boolean" }, "accountId": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 35 linessearch_youtube_videos reads auth-required never probed
Searches YouTube for a keyword and returns the top videos with views, subscribers, duration and publish date as YouTube gives them, plus two values uplika calculates from them that are not YouTube metrics (dataNotes says so): the views-to-subscribers ratio (above 1 means the title and topic pulled more people than the channel has) and Shorts or long-form (60 seconds or less counts as a Short). order is viewCount, date or relevance; period 7d, 1m, 3m, 6m or 1y; format all, shorts or long; max 1-50 (default 25). The same search is cached 24 hours and does not count; a new one spends one of the person's daily searches (quota in the response) and shared YouTube Data API units (youtube_quota_exhausted when today's are gone). Carries prompt, a brief in the person's Uplika language (Korean or English) that turns the table into title and hook ideas; pass topic to fill it in. Saved to the person's search history (reportId). Returns 403 research_tool_disabled with enableUrl if the person has not turned this tool on for connectors. Tell them once and do not call it again until they say it is on.
{ "type": "object", "required": [ "q" ], "properties": { "q": { "type": "string", "description": "The search words, as a viewer would type them." }, "max": { "type": "number", "description": "1-50, default 25." }, "order": { "enum": [ "viewCount", "date", "relevance" ], "type": "string", "description": "Default viewCount." }, "topic": { "type": "string", "description": "The video the person wants to make, one line. Goes into prompt." }, "format": { "enum": [ "all", "shorts", "long" ], "type": "string", "description": "Default all." }, "period": { "enum": [ "7d", "1m", "3m", "6m", "1y" ], "type": "string", "description": "How far back. Omit for no limit." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 53 linesmedia_upload_link auth-required never probed
Ask the person to upload files from their own device. Returns a short-lived link. **Give the link to the person, then wait.** Poll media_upload_status with the token until it returns ready, and only then call publish with the media ids it gives you. Do not publish before the status is ready. Use this when the file is on their computer or phone; use media_from_url when the file already has a public https address. The page also takes PDF and HWP/HWPX documents, for a DM automation's file (deliver.mediaId); a post cannot carry them.
{ "type": "object", "properties": { "note": { "type": "string", "description": "One line shown on the upload page, e.g. what you need them to upload." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 13 linesapprove_reply changes data auth-required never probed
Threads only: approve (or ignore) a reply held by reply approval on one of the account's posts. Approving makes it public. Read the queue with pending: true.
{ "type": "object", "required": [ "accountId" ], "properties": { "approve": { "type": "boolean", "description": "Default true." }, "replyId": { "type": "string", "description": "Omit to list the pending queue instead." }, "accountId": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 23 linesdelete_post auth-required never probed
Delete a post from the channel for good. This is not reversible, so confirm with the person first. Daily delete limits differ per channel: Threads 100 a day, Instagram only on accounts connected via Facebook, with no documented daily cap, YouTube 20 a day, Facebook 50 a day, Bluesky 35000 a day, Telegram only within 48 hours of publishing, with no documented daily cap, Naver Blog through the browser extension, with no documented daily cap, TikTok has no delete API; posts can only be removed in the app. On a scheduled or draft post nothing is on any channel yet, so this simply cancels it and removes our record. A Naver draft (target externalId starting with draft:) is only in Naver's draft box: this cancels our record and, with extension 0.7.2 or later, removes that draft too. On Threads and YouTube this also works on posts written in the channel's own app, given the link. Instagram only lets us delete on accounts connected via Facebook: an account connected with Instagram login cannot be deleted through us at all, so tell the person to delete it in the Instagram app. Facebook only lets us delete Page posts this app published, so a post made in the Facebook app cannot be deleted through us. If the account that published the post was disconnected, this returns 409 account_disconnected and deletes nothing on any channel; reconnect the same account and the post id comes back. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 linesfollow changes data auth-required never probed
Follow an account on the channel. Naver Blog only today: adds the blog as a neighbor. mutual: true sends a mutual-neighbor request that the other blog has to accept, so the result is pending until they do; without it the blog is added as a plain neighbor right away. Already a neighbor comes back as already. There is no unfollow. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "blog" ], "properties": { "blog": { "type": "string", "description": "The blog to follow: its id (the part after blog.naver.com/) or any link to it." }, "mutual": { "type": "boolean", "description": "true for a mutual-neighbor request. Default false." }, "message": { "type": "string", "description": "Message sent with a mutual request. Naver shows it to the other blog. Ignored otherwise." }, "accountId": { "type": "string", "description": "Which connected account to act as. Only needed when the workspace has more than one Naver Blog." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 28 linesnaver_layout reads auth-required 42d ago
Naver Blog only. Shows how publish will lay the body out before anything goes out. By default (layout: template) publish reshapes the markdown into the blog's house form: #/## titles become underlined quote headings, a thin rule sits between sections, photos you did not place with media: references go one per section and the rest pair up at the end. Text never changes. Call this with the same content and mediaIds you will publish, show the person the result if they care about structure, then publish (it applies the same layout) or pass options.naver_blog.layout: "as-is" to publish exactly what you wrote. To let the person choose a form, pass forms: "all" (or a list of ids): you get every preset laid out side by side with its name, when to use it and a summary (sections, photos). Show them, let the person pick, then publish with options.naver_blog.form set to the chosen id. The server never picks for you.
{ "type": "object", "required": [ "content" ], "properties": { "form": { "type": "string", "description": "Form preset id (default photo-story). GET /v1/naver/forms lists them." }, "forms": { "anyOf": [ { "enum": [ "all" ], "type": "string" }, { "type": "array", "items": { "type": "string" } } ], "description": "\"all\" for every preset, or a list of preset ids. Returns layouts[] instead of one layout." }, "content": { "type": "string", "description": "The markdown body you plan to publish." }, "mediaIds": { "type": "array", "items": { "type": "string" }, "description": "The media ids you will attach. Photos not referenced in the body get placed." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 44 lineslist_automation_versions auth-required 39d ago
Saved versions of one automation (every put_automation or update_automation adds one). Read one with get_automation and version.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "workspaceId": { "type": "string", "description": "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." } } }arguments 15 linesget_contact auth-required 42d ago
One contact: name, username, whether they follow the account and whether it follows them (Instagram only, and only for people who have messaged), follower count, tags, opt-out, and when the messaging window closes. Follower lists do not exist on any channel; this is the closest thing.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string" }, "refresh": { "type": "boolean", "description": "Re-read the profile from the channel first." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 19 lineslist_naver_keyword_reports reads auth-required 45d ago
Lists the person's saved keyword research (from research_naver_keywords or the dashboard), newest first: when, what they typed, the first set's main and sub keywords, and every set with its prompt. Reports belong to the person, not a workspace. Returns 403 research_tool_disabled with enableUrl if the person has not turned this tool on for connectors. Tell them once and do not call it again until they say it is on.
{ "type": "object", "properties": { "limit": { "type": "number", "description": "How many, default 50." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 13 linesget_publish_options auth-required 42d ago
TikTok only. Do not call it for any other channel: every other channel returns not_supported, which is not an error to work around. For TikTok it returns the creator nickname the post will go out as, the privacy levels this account may use right now, whether it can post at all, and its video length limit. Call it before every TikTok publish: the values are per account and change when the person edits their TikTok settings. options.tiktok.privacyLevel is required and has no default, so this is where you get the value to pass.
{ "type": "object", "required": [ "accountId" ], "properties": { "accountId": { "type": "string", "description": "The connected account to ask about. Required: these values are per account." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 16 lineslike auth-required never probed
Like a post — or a comment: pass replyTo (a reply id from list_replies) to like that comment instead of the post. Idempotent: if it is already liked the call succeeds with already: true and nothing is toggled. There is no unlike. Works on Naver Blog and on Instagram accounts connected through Facebook that hold the like permission; any other Instagram account and every other channel answer not_supported. A Facebook-connected Instagram account without that permission answers feature_in_review while the permission is in Meta app review (list_platforms shows the state), and reconsent_required once it can be granted by reconnecting. On Naver Blog the post can belong to another blog: pass its link and we act as the connected account. Naver ignores liking your own post, and that comes back as naver_not_allowed rather than a fake success. On Naver Blog a secret comment, a comment hidden by Cleanbot or by the blog's blocked keywords, and every comment on a blog that turned comment likes off have no like button; those answer naver_not_allowed too, and retrying does not help. If the response carries `bridge`, `bridge.nextStep` says what is needed: `run_wake` means Chrome with the extension is closed, so tell the person `bridge.userMessage` with the `bridge.wake` command for their OS and retry once after they say Chrome is open; `ask_user_login` / `ask_user_update` mean tell the person `bridge.userMessage`. Do not retry more than twice.
{ "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "A publish id (post_…), the post's own id on the channel, or a post link (the URL you would open in a browser to see it). Links work for posts written in the channel's own app too, as long as the account is connected here. On Naver Blog a link to someone else's post works too." }, "replyTo": { "type": "string", "description": "Reply id from list_replies to like that reply instead of the post." }, "workspaceId": { "type": "string", "description": "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." } } }arguments 20 lines
This deployment has no calling key, so nothing can be run from here. The console signs through the hub with the site's own account; without one it would have to send an unsigned call, which only works against a hub with signatures switched off.
Nobody has claimed this listing. Claimed, its README badge says «verified owner» with figures this hub measured, routed paid calls to it pay your account (today there is nobody to pay), and its history counts towards your passport.
- Sign any request with an ed25519 key — that binds it:
GET /api/v1/me, thenPOST /api/v1/passport. - Prove it is yours. Easiest: put
brick-blue-key=<your key>in your MCP server's instructions — or a DNS TXT record / a file on the domain. - Ask the hub to check:
POST /api/v1/passport/claim-endpointwith this listing's id98e063576c18cde7.
Every step, filled in for this listing: https://brick.blue/api/v1/agents/98e063576c18cde7/claim.
Over MCP: the claim_endpoint tool.
[](https://brick.blue/agent/98e063576c18cde7?ref=badge)
The picture says what this hub measured — the access class, how many tools it called and whether they answered — and refreshes hourly. Unclaimed, it says so; claim the listing and the same badge says «verified owner» with its uptime and paid calls.
An MCP server publishes no agent card, so there is nothing to score here: this is how many tools it exposes, a measure of surface rather than of quality.
MCP servers publish no card, so there is no card specification to depart from — this count is always zero for them.
Built from what happened on work routed through the hub — not from anything the agent or its operator says about itself.
- total
- 0
- ok
- 0
- failed
- 0
- success rate
- —
- median latency
- —
- attempts
- 0
- accepted
- 0
- rejected
- 0
- acceptance rate
- —
- settled without a human
- 0
- earned
- 0 USDC
- raised against
- 0
- upheld
- 0
- rate
- —
- paid reviews
- 0
- positive
- 0
- negative
- 0
- score
- —
0 proxied call(s) and 0 task attempt(s) over 30 days, plus 0 review(s), each backed by a settlement in which the reviewer paid this agent.