REST API

Posts

Publish, read, retry and delete.

GET/v1/posts

Recent publishes, newest first. Replies are not listed as posts.

Paging

NameTypeDescription
limitquery1-100, defaults to 20.
beforequeryA publish id from a previous page's nextBefore. Returns the ones older than it.
includeDeletedquerytrue brings back posts you deleted from the channel. They are hidden by default. Our record and its metrics stay either way.
statusqueryNarrow to one or more statuses, comma separated: scheduled, draft, publishing, published, partial, failed or cancelled. Omit for everything, scheduled and draft included.
sincequeryYYYY-MM-DD or ISO 8601. Posts dated at or after this. A scheduled post is dated by scheduledAt, a sent one by when it was created.
untilqueryYYYY-MM-DD or ISO 8601. Posts dated at or before this; a date means the whole day.
sortqueryscheduled orders by the post's date ascending and drops paging. Omit for newest first.
hasMoreresponseThe list was cut short.
nextBeforeresponsePass it back as before. Absent when there is nothing more.

POST/v1/posts

Publish to one or more connected accounts.

NameTypeDescription
contentRequiredstringPost text. Emoji count as UTF-8 bytes.
accountIdsRequiredstring[]Ids from GET /v1/accounts.
mediaIdsstring[]Media already confirmed with complete. On Naver Blog you may leave out ids the body already points at with media:<id>; we read those from the text and attach them.
optionsobjectPer-channel settings, keyed by the channel id. The shape is in GET /v1/platforms under options and on each channel page. Some channels cannot publish without it — YouTube needs options.youtube.title and Naver Blog needs options.naver_blog.title plus a category. Every channel takes options.<channel>.content, a different text for that channel alone.
scheduledAtstringISO 8601 with a timezone offset, 10 minutes to a year out. We keep the post as scheduled and send it at that time, on every channel.
draftbooleantrue saves the post without sending. It can have no accountIds yet. Send it later with POST /v1/posts/{id}/publish.
batchIdstringOptional tag, up to 40 characters, to group posts made together (for example the same text sent to two workspaces). Comes back on every post.
replyTostringPublish this as a reply to that post id.
threadItemsobject[]Split a long post into a chain: [{ content, mediaIds? }]. The first item is the root, the rest become replies under it in order. Each item obeys the character limit on its own. Exclusive with content. Only Threads and Bluesky take a chain; the composer builds one with the item editor. If a later item fails the ones already up stay up, and the response carries thread.resumeFrom. Posts that have later items carry them back as threadItems, and PATCH takes the same field while the post is scheduled or a draft.
topicTagstringOne topic to tag the post with. Threads takes exactly one and rejects periods and ampersands. On a thread it applies to the first piece only.
Idempotency-KeyheaderSend the same key when you retry and we return the first answer instead of publishing twice. Reusing a key with a different body is a 422. Kept for 24 hours.
waitbooleanHold the response until the post is really out. Text waits up to 10 seconds, media up to 75. Past that you get the usual 202 with a next hint, and the post keeps going. Defaults to false.
curl -X POST https://api.uplika.com/v1/posts \
  -H "authorization: Bearer $UPLIKA_KEY" \
  -H "content-type: application/json" \
  -d '{
    "content": "Now with images",
    "accountIds": ["acc_01H..."],
    "mediaIds": ["med_01H..."]
  }'

GET/v1/posts/{id}

One publish with the status of each target.

Target status

NameTypeDescription
publishingstatusIn flight at the channel.
publishedstatusCarries externalId and url.
partialstatusSome channels went out and some did not. Retry sends only the ones that failed. There is no 207 — publishing is 202 plus polling, so nothing is held open.
cancelledstatusDeleted from the channel. Our record and its metrics stay. Hidden from GET /v1/posts unless you pass includeDeleted=true.
failedstatusCarries error.
scheduledstatusHeld until scheduledAt. Nothing is on any channel yet. Editable.
draftstatusSaved without a time. May have no targets. Editable.

A publish has one row per channel. publishing is in flight, published carries the channel's own id and URL, failed carries a reason.

Scheduling and drafts

Pass scheduledAt and we keep the post as scheduled and send it at that time, on every channel, from our side. Pass draft: true and nothing goes out until someone calls publish or the dashboard does. Both come back with status scheduled or draft, can be changed with PATCH, sent early with /publish, and removed with DELETE. scheduledAt needs a timezone offset and is 10 minutes to a year out. Some channels also have scheduling of their own (options.youtube.publishAt, options.facebook.scheduledPublishTime, options.naver_blog.scheduledAt); those still work over the API but the dashboard hides them, because two clocks on one post is a contradiction waiting to happen.

Different text per channel

Every channel takes options.<channel>.content. When set it replaces the shared content for that channel only, and the length and media checks use it. That is how a 500-character Threads post and a Naver Blog Markdown body fit in one call. GET /v1/platforms lists what else each channel takes, under options.

PATCH/v1/posts/{id}

Change a scheduled or draft post before it goes out. Fields you leave out keep their value, except that a post going only to Naver Blog takes its media list from the new body's media: references when you leave mediaIds out; options replaces the whole object; threadItems replaces a chain, content alone edits its first item. Editing a post that is already live is asynchronous: wait: true holds the response for the result (up to 75 seconds), and past that it answers 202 with next telling you to read the post. A Naver draft save that went quiet is looked up in Naver's draft list and recovered when it is there.

NameTypeDescription
contentstringNew post text.
accountIdsstring[]New target accounts. Replaces the current set.
mediaIdsstring[]New media, in order. Replaces the current set. On a post going only to Naver Blog, leave it out when the body places media with media: references: those references become the list and media the new body does not place is removed (the response note says how many).
optionsobjectPer-channel settings. Replaces the whole object.
scheduledAtstringNew send time, same rules as on create. null turns the post into a draft.
draftbooleantrue turns a scheduled post back into a draft.

POST/v1/posts/{id}/publish

Send a scheduled or draft post right now. Same function the scheduler runs, so it cannot go out twice.

NameTypeDescription
waitbooleanHold the response until the post is really out, same as on create.

POST/v1/posts/{id}/retry

Retry the targets that failed. Targets that already went out are skipped. A Naver target that stopped right after Save as draft (naver_draft_unknown) or a Naver reply that did not confirm in time (naver_reply_unknown) answers 409 until the body carries {"force": true}. If another post with the same content and files is already on the same account, it answers 409 duplicate_post with that post's id and retries nothing; {"force": true} sends it anyway.

DELETE/v1/posts/{id}

Delete the post from the channel and mark our record deleted.

On a scheduled or draft post nothing is on any channel yet, so DELETE simply removes our record. On a post that went out it deletes from the channel first and keeps our record marked deleted.

Reading what is already on the channel

Our history only has what went out through us. These four read the channel itself and pick which channels to publish to. They are the routes behind list_channel_posts, get_quota, open_post and select_channels, so an agent gets the same answers over REST.

GET/v1/posts/{id}/thread

One post with its replies and metrics in a single call.

GET/v1/channels/posts

What is on the channel right now, including posts written in the app.

Query

NameTypeDescription
accountIdqueryLimit to one connected account. Omit to cover every channel.
limitquery1-100, defaults to 25.

GET/v1/channels/quota

How much of the 24 hour allowance the channel says is used.

GET/v1/channels/bridge

Whether the Naver Blog bridge (browser extension) is online, and how to wake Chrome if it is not.

GET/v1/channels/publish-options

What a channel needs to know before you publish to it. TikTok only: the creator nickname the post goes out as, the privacy levels this account may use right now, whether it can post at all, and its video length limit. Other channels return not_supported.

NameTypeDescription
accountIdqueryRequired. These values are per connected account and change when the person edits their channel settings, so do not cache them.

POST/v1/accounts/select

Pick which connected channels to publish to.

POST/v1/accounts/:id/refresh

Re-read a channel's metadata, such as Naver Blog categories. Waits up to a minute for the browser extension.

NameTypeDescription
scopestring | string[]Omit to list candidates. "all" for every active channel, or an array mixing platform names, handles and account ids.

GET/v1/naver/blog/diagnose

Read a Naver blog's public RSS feed (its latest posts, at most 50) and say 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 (candidates to research, not proven keywords). The response carries the date range it read, so a quiet blog's 50 posts are not mistaken for everything. Cached 24 hours across users; a cache miss spends one of 20 diagnoses per person per day. No Naver API keys are needed.

NameTypeDescription
blogRequiredstringA Naver blog id (2-40 letters, digits, - or _) or any link to the blog: blog.naver.com/<id>, m.blog.naver.com/<id>/<logNo>, PostView.naver?blogId=<id>.

POST/v1/naver/keywords/judge

Measure Naver search keywords and pick a set for one post. Expands nothing; send the keywords you have. Returns monthly searches (Search Ad), blog document count and posts per month (API HUB), a verdict per keyword, and sets of one main keyword plus two to five sub keywords with the same intent. Results are cached for seven days across users; 30 calls and 300 measured keywords per user per day.

NameTypeDescription
keywordsstring[]Required. 1-300 keywords. Spaces are fine; the numbers come back per keyword as you sent it.
savebooleanDefault false. true stores this research as a report (the keywords, the first set's main and subs, every set with its prompt) and returns reportId. The same person sending the same keyword set within ten minutes gets the existing reportId back with no new row and no judge call spent.
seedTextstringWhat the person typed, kept with the saved report as its label. Defaults to the first five keywords.
cacheOnlybooleanDefault false. true never calls Naver: keywords without a fresh cached value come back in unmeasured with no numbers. Use it after measuring in chunks, so the judge call is instant and spends nothing upstream.
budgetMsnumberTime budget for new measurements, default and maximum 60000. When it runs out, no new upstream call starts; the response says partial: true and lists unmeasured. Call again and the cached part is skipped.

POST/v1/naver/keywords/measure

Same measurement without the verdict and sets. withDocs:false skips document counts.

NameTypeDescription
keywordsstring[]Required. 1-300 keywords. At most 40 of them are measured fresh in one call; the rest come back in unmeasured and are picked up by the next call.
withDocsbooleanDefault true. false measures search volume only.
budgetMsnumberTime budget for new measurements, default and maximum 60000. Past it the call stops starting upstream requests and returns partial: true with unmeasured.

POST/v1/naver/keywords/expand

Expand seed keywords one level through Naver autocomplete, for agents and API keys (the dashboard does this in your browser). When the person's uplika Chrome extension is on, it looks the seeds up in the background from their own connection; otherwise our server does. Returns every word found with which seed produced it, and via says which path answered. When neither can run this answers 503 naver_autocomplete_unavailable, and you should pass your own keyword list to judge instead.

NameTypeDescription
seedsstring[]Required. 1-10 seed keywords. Each is sent to Naver autocomplete once (one level); the words that come back are candidates to measure with judge, not proven keywords. Cached seven days across users. No daily quota: when the person's uplika Chrome extension answers there is no wait, and when our server answers, one call per person every 60 seconds (naver_autocomplete_busy with retryAfterSeconds).

What comes back

NameTypeDescription
found{ keyword, from }[]Every word, the seeds first. from is seed, or L1:<seed> for a word that seed produced.
errors{ query, error }[]Seeds that could not be looked up, with why.
cachednumberHow many seeds came from the seven-day cache.
via{ cache, extension, server }How many seeds each path answered: the shared cache, the person's Chrome extension (in the background, from their own connection), or our server.

GET/v1/naver/keywords/reports

List the keyword research you saved (judge with save: true), newest first: when, what you typed, the first set's main and sub keywords, and every set with its ready-to-paste prompt. Reports belong to the person, so an API key limited to some workspaces gets 403.

NameTypeDescription
limitnumberDefault 50, newest first.

GET/v1/naver/keywords/history

The measurement history of one keyword: one point per day it was actually measured (cache hits add nothing), newest first, with search volume, document count and posts per month. An empty list is 200 with data: [].

NameTypeDescription
keywordRequiredstringThe keyword, as you would send it to measure. A query parameter, since keywords are usually Korean.
limitnumberDefault 100 days, newest first.
NameTypeDescription
qRequiredstringThe search words, as a viewer would type them.
orderstringviewCount (default), date or relevance.
periodstring7d, 1m, 3m, 6m or 1y. Omit for no limit.
formatstringall (default), shorts or long. 60 seconds or less counts as a Short.
maxnumber1-50, default 25.
topicstringThe video the person wants to make, one line. Goes into prompt and is saved with the report.

GET/v1/research/youtube/reports

The person's saved YouTube searches, newest first: when, the search and its filters, the topic, how many videos and whether it came from cache. Results are kept 90 days.

NameTypeDescription
limitnumberDefault 50, newest first.

GET/v1/research/youtube/reports/{id}

One saved search with the videos as they were and the prompt built from them.

POST/v1/naver/seo/check

Run the Naver Blog placement checks on a Markdown body before publishing: section titles match the sub keywords one to one, the main keyword appears 3-5 times, 12-15 tags all present in the body, no product names in the first three paragraphs, every attached image placed with a caption of ten or more characters, and a body length floor. Nothing is blocked; the report says what is off and why.

NameTypeDescription
contentstringRequired. The Markdown body, up to 60,000 characters.
titlestringThe post title.
mainstringThe main keyword. Without it the keyword checks cannot pass.
subsstring[]Sub keywords, one per section title.
tagsstring[]The tags you will send in options.naver_blog.tags.
fixedTagsstring[]Tags that need not appear in the body (brand tags).
productsstring[]Product and brand names that must stay out of the first three paragraphs.
mediaIdsstring[]The media ids attached to the post, to check that each is placed.

POST/v1/naver/layout

See how a body will be laid out before publishing. The same pass publish runs by default (layout: template), returned without sending anything; forms: "all" lays it out in every preset so a person can pick one.

NameTypeDescription
contentRequiredstringThe markdown body to lay out.
mediaIdsstring[]Attached media, in order. Images the body does not reference are placed by the form.
formstringOne preset id (default photo-story). Ignored when forms is given.
forms"all" | string[]Lay the same body out in several presets at once; the response carries layouts[] so a person can compare and pick.

GET/v1/naver/forms

The seven post forms we ship. A form is the shape of a post: what opens a section, whether a quotation is a heading or a quoted passage, how often a sentence is highlighted, how many images per section, whether images get captions. Each one was measured from a real published post. Pass an id to options when generating; the form only shapes what we ask the model for, and never touches publishing.

GET/v1/naver/drafts

The drafts in the blog's draft box: posts saved with draftOnly and anything saved by hand in the Naver editor. Extension 0.7.2 or later.

POST/v1/naver/drafts/{logNo}/publish

Publishes a draft from the draft box exactly as it is in Naver: the extension loads it in the editor and presses publish, so hand edits are kept. Same response shape as POST /v1/posts; if uplika made that draft, that post record flips to published.

NameTypeDescription
logNoRequiredstringThe draft's id from GET /v1/naver/drafts.
accountIdstringRequired only when the workspace has more than one Naver Blog account.
title / categoryId / category / tags / openTypestringOverride the draft's own values. Left out, the draft goes out as it is.
waitbooleanHold until the extension finishes, up to 75 seconds like a post with media on POST /v1/posts. Past that you get the usual 202 with a next hint, and the draft keeps going out.

GET/v1/naver/grammar

What the Naver Blog body understands: directives, block and inline markup, attributes, highlights, media references and the limits. This is the same guide the describe_grammar tool returns, and the values come from one source, so it never drifts from what the editor actually accepts. Pass topic to read one section.

NameTypeDescription
topicstringOne section instead of the whole guide. The 400 lists the ids.

POST/v1/naver/forms/extract

Read a public Naver Blog post and hand back its form. We take the structure and the formatting, never the words or the photos. Use it for "write it like this post". We keep what we read, so AI generation uses that saved form and never goes back to Naver. Call it again to pick up changes in the reference.

NameTypeDescription
urlstringRequired. A public Naver Blog post, like https://blog.naver.com/someone/224349824125. Only blog.naver.com links work, and private or deleted posts are refused. Five per minute.

GET/v1/naver/forms/snapshot

When a reference post was last read, and how many blocks and photos it had. Reads our saved copy only.

NameTypeDescription
urlstringRequired. The same post link you passed to extract. Answers from our database only; Naver is not called. 404 reference_not_read when it was never read.
publishing ──► published     the channel accepted it, permalink is set
           └─► partial       some channels made it, some did not
           └─► failed        none of them made it, error says why

failed     ──► publishing   retry, and only the targets that failed go again
published  ──► cancelled    you deleted it; our record and its metrics stay

Common questions

How do I publish to several channels at once?

Name them all in accountIds on a single call. One request creates one publish with one row per channel, so you read the outcome in one place instead of correlating separate calls.

What does a 202 actually mean?

That we accepted the post and the channels have not answered yet. Read the per-target status to know what landed; the top-level response only says the work started.

Can I delete a post after it went out?

Yes. Deleting removes it from the channel and marks our record deleted, so the history still shows that it existed and when it went.