REST API
Posts
Publish, read, retry and delete.
GET/v1/posts
Recent publishes, newest first. Replies are not listed as posts.
Paging
| Name | Type | Description |
|---|---|---|
limit | query | 1-100, defaults to 20. |
before | query | A publish id from a previous page's nextBefore. Returns the ones older than it. |
includeDeleted | query | true brings back posts you deleted from the channel. They are hidden by default. Our record and its metrics stay either way. |
status | query | Narrow to one or more statuses, comma separated: scheduled, draft, publishing, published, partial, failed or cancelled. Omit for everything, scheduled and draft included. |
since | query | YYYY-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. |
until | query | YYYY-MM-DD or ISO 8601. Posts dated at or before this; a date means the whole day. |
sort | query | scheduled orders by the post's date ascending and drops paging. Omit for newest first. |
hasMore | response | The list was cut short. |
nextBefore | response | Pass it back as before. Absent when there is nothing more. |
POST/v1/posts
Publish to one or more connected accounts.
| Name | Type | Description |
|---|---|---|
contentRequired | string | Post text. Emoji count as UTF-8 bytes. |
accountIdsRequired | string[] | Ids from GET /v1/accounts. |
mediaIds | string[] | 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. |
options | object | Per-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. |
scheduledAt | string | ISO 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. |
draft | boolean | true saves the post without sending. It can have no accountIds yet. Send it later with POST /v1/posts/{id}/publish. |
batchId | string | Optional tag, up to 40 characters, to group posts made together (for example the same text sent to two workspaces). Comes back on every post. |
replyTo | string | Publish this as a reply to that post id. |
threadItems | object[] | 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. |
topicTag | string | One 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-Key | header | Send 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. |
wait | boolean | Hold 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
| Name | Type | Description |
|---|---|---|
publishing | status | In flight at the channel. |
published | status | Carries externalId and url. |
partial | status | Some 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. |
cancelled | status | Deleted from the channel. Our record and its metrics stay. Hidden from GET /v1/posts unless you pass includeDeleted=true. |
failed | status | Carries error. |
scheduled | status | Held until scheduledAt. Nothing is on any channel yet. Editable. |
draft | status | Saved 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.
| Name | Type | Description |
|---|---|---|
content | string | New post text. |
accountIds | string[] | New target accounts. Replaces the current set. |
mediaIds | string[] | 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). |
options | object | Per-channel settings. Replaces the whole object. |
scheduledAt | string | New send time, same rules as on create. null turns the post into a draft. |
draft | boolean | true 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.
| Name | Type | Description |
|---|---|---|
wait | boolean | Hold 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
| Name | Type | Description |
|---|---|---|
accountId | query | Limit to one connected account. Omit to cover every channel. |
limit | query | 1-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.
| Name | Type | Description |
|---|---|---|
accountId | query | Required. 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.
| Name | Type | Description |
|---|---|---|
scope | string | string[] | Omit to list candidates. "all" for every active channel, or an array mixing platform names, handles and account ids. |
| Name | Type | Description |
|---|---|---|
blogRequired | string | A 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>. |
| Name | Type | Description |
|---|---|---|
keywords | string[] | Required. 1-300 keywords. Spaces are fine; the numbers come back per keyword as you sent it. |
save | boolean | Default 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. |
seedText | string | What the person typed, kept with the saved report as its label. Defaults to the first five keywords. |
cacheOnly | boolean | Default 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. |
budgetMs | number | Time 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. |
| Name | Type | Description |
|---|---|---|
keywords | string[] | 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. |
withDocs | boolean | Default true. false measures search volume only. |
budgetMs | number | Time budget for new measurements, default and maximum 60000. Past it the call stops starting upstream requests and returns partial: true with unmeasured. |
| Name | Type | Description |
|---|---|---|
seeds | string[] | 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
| Name | Type | Description |
|---|---|---|
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. |
cached | number | How 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. |
| Name | Type | Description |
|---|---|---|
limit | number | Default 50, newest first. |
| Name | Type | Description |
|---|---|---|
keywordRequired | string | The keyword, as you would send it to measure. A query parameter, since keywords are usually Korean. |
limit | number | Default 100 days, newest first. |
GET/v1/research/youtube/search
Search YouTube for a keyword and get the top videos with views, subscribers, the views-to-subscribers ratio, Shorts or long-form, length and publish date, plus a ready-to-paste Korean prompt for titles and hooks. 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. Saved to the person's history with reportId.
| Name | Type | Description |
|---|---|---|
qRequired | string | The search words, as a viewer would type them. |
order | string | viewCount (default), date or relevance. |
period | string | 7d, 1m, 3m, 6m or 1y. Omit for no limit. |
format | string | all (default), shorts or long. 60 seconds or less counts as a Short. |
max | number | 1-50, default 25. |
topic | string | The 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.
| Name | Type | Description |
|---|---|---|
limit | number | Default 50, newest first. |
GET/v1/research/youtube/reports/{id}
One saved search with the videos as they were and the prompt built from them.
| Name | Type | Description |
|---|---|---|
content | string | Required. The Markdown body, up to 60,000 characters. |
title | string | The post title. |
main | string | The main keyword. Without it the keyword checks cannot pass. |
subs | string[] | Sub keywords, one per section title. |
tags | string[] | The tags you will send in options.naver_blog.tags. |
fixedTags | string[] | Tags that need not appear in the body (brand tags). |
products | string[] | Product and brand names that must stay out of the first three paragraphs. |
mediaIds | string[] | The media ids attached to the post, to check that each is placed. |
| Name | Type | Description |
|---|---|---|
contentRequired | string | The markdown body to lay out. |
mediaIds | string[] | Attached media, in order. Images the body does not reference are placed by the form. |
form | string | One 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. |
| Name | Type | Description |
|---|---|---|
logNoRequired | string | The draft's id from GET /v1/naver/drafts. |
accountId | string | Required only when the workspace has more than one Naver Blog account. |
title / categoryId / category / tags / openType | string | Override the draft's own values. Left out, the draft goes out as it is. |
wait | boolean | Hold 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. |
| Name | Type | Description |
|---|---|---|
topic | string | One section instead of the whole guide. The 400 lists the ids. |
| Name | Type | Description |
|---|---|---|
url | string | Required. 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. |
| Name | Type | Description |
|---|---|---|
url | string | Required. 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 stayCommon 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.