REST API
에러
모양은 하나, 코드는 구체적으로, 값은 다룰 수 있게.
모양
실패는 code 와 영어 message 를 가진 error 객체로 옵니다. message 에 끼워 넣은 값은 따로 필드로도 보냅니다. 그 값으로 자기 언어의 문장을 다시 만들 수 있습니다.
Idempotency-Key 헤더를 주면 두 번 올리는 대신 저장해 둔 응답을 그대로 돌려줍니다. 성공한 결과를 24시간 보관하고, 같은 키에 다른 본문이 오면 idempotency_key_reused 로 거절합니다. 층이 둘입니다. 키는 이 요청을 이미 실행했는지를 묻고, 내용 지문은 같은 글과 파일이 같은 계정으로 10분 안에 다시 왔는지를 물어 duplicate_post 로 거절합니다. 실패한 글을 다시 보낼 때도 같은 지문을 보고, 10분이 지났어도 같은 계정에 같은 글의 다른 글이 있으면 duplicate_post 로 거절합니다. 의도한 재발행이면 force: true 를 주세요.
위반이 여럿이면 한 번에 옵니다
규칙을 둘 이상 어기면 error.errors 에 전부 담기고, 첫 항목은 error 자리에도 그대로 펼쳐집니다. 한 번에 고치면 됩니다. 재시도할 때마다 다음 위반을 하나씩 듣지 않아도 됩니다. 둘만 예외로 혼자 옵니다 — no_accounts 와 empty_content 는 나머지 검사의 전제입니다.
{
"error": {
"code": "content_too_long",
"message": "Threads allows 500 characters. This is 620.",
"platform": "threads",
"limit": 500,
"actual": 620
}
}코드
| 이름 | 타입 | 설명 |
|---|---|---|
verification_required | 401 | Email verification is not finished. |
next_post_taken | 409 | Another enabled automation is already waiting for the next post on this account. Only one can; it frees the slot once a post binds to it. |
past_comments_not_supported | 400 | Sending an automation to past comments works on Instagram and Facebook. This channel's comments cannot be read back for it. |
past_comments_no_trigger | 400 | The automation has no comment trigger, so there are no comments it would react to. |
automation_not_live | 409 | The automation is a draft. Enable it first, then send it to past comments. |
past_comments_no_post | 409 | The automation is waiting for the next post, so there is no post whose comments it could read yet. |
past_comments_running | 409 | The automation is already sending to past comments. Wait for it to finish, or stop it with DELETE. The running job comes back in pastComments. |
scheduled_at_invalid | 422 | scheduledAt is not ISO 8601 or has no timezone offset. |
scheduled_at_range | 422 | scheduledAt is under 10 minutes or over a year from now. |
draft_and_schedule | 422 | Both draft and scheduledAt were passed. Pick one. |
batch_id_invalid | 422 | batchId is longer than 40 characters or has characters other than letters, digits, - and _. |
naver_source_required | 409 | update_post sent a new body for a Naver Blog post written in Naver's editor without the sourceVersion of its current source, or the post changed on Naver since that source was read. A body made from an old copy would lose its photos, videos, cards, tables, links and formatting. The response carries source (the post's current body with @keep lines), keep (their ids) and sourceVersion. Edit source and send it back with sourceVersion; delete a @keep line only to remove that block. Nothing was changed. |
naver_keep_unknown | 422 | A @keep(...) id is not a block of the post being rewritten, or @keep was used outside update_post on a post written in Naver's editor. Use the ids from its current source (naver_source_required returns it). Nothing was changed. |
post_not_editable | 409 | PATCH or /publish on a post that already went out on a channel that cannot rewrite it, or retry on one that has not. The message is the channel's reason from list_platforms (feature update, or update_imported for a post written directly on the channel). Scheduled and draft posts can always be changed. |
live_post_no_schedule | 422 | PATCH on a published post with scheduledAt or draft: a post that already went out has no schedule or draft state to change. Pass content, mediaIds or options instead. |
live_post_accounts_fixed | 422 | PATCH on a published post with a different accountIds. A published post keeps its channels; pass the same ids or leave them out. |
nothing_to_change | 422 | PATCH on a published post with none of content, mediaIds or options. There is nothing to rewrite. |
target_required | 422 | An engagement call (like, reply, follow) without a target. Pass postId, or ownerHandle and externalId; follow takes ownerHandle. |
thread_piece | 409 | PATCH, /publish or DELETE aimed at a later item of a thread. Sends rootId; use the first item instead. |
naver_draft_not_found | 404 | publish_naver_draft with a logNo that is not in the blog's draft box. list_naver_drafts shows what is there. |
account_required | 422 | The workspace has more than one account on that channel. Sends accounts; pass accountId. |
invalid_api_key | 401 | The bearer token is not a valid key. |
forbidden_scope | 403 | The key lacks the scope. Sends scope. |
no_accounts | 422 | accountIds was empty. Sends candidates, and next when nothing is connected. Sends needsReconnect and a reconnect hint when the channels you have are expired rather than missing. |
empty_content | 422 | Neither text nor media was given. |
account_not_connected | 404 | One of the accountIds is not connected. Sends candidates. |
account_expired | 422 | The channel token expired. Sends platform. |
platform_not_available | 422 | That channel is not live yet. Sends platform. |
content_too_long | 422 | Sends platform, limit and actual. |
media_required | 422 | That channel cannot post text alone. Sends platform. |
media_mixed | 422 | No longer sent for a publish (2026-09-17). A channel that cannot mix images and video keeps the video and drops the images; the target's warning says what was dropped. TikTok still answers with it if a mixed set reaches the channel some other way. |
media_too_many | 422 | No longer sent (2026-09-17). Over the channel's image, video or carousel limit, the first items up to the limit go out and the target's warning says so. Read the limits from GET /v1/platforms. |
media_too_large | 422 | Sends limit, limitMb and actual. Video fetched by media_from_url has its own cap, lower than presign; the response carries limitMb. Larger videos go through media_upload_link or presign. |
media_aspect_ratio | 422 | Sends limit and actual. |
media_dimensions | 422 | Video wider than the limit. Sends limit and actual. Image width is never a reason. |
media_type_not_supported | 422 | Sends types. |
media_too_long | 422 | Video longer than the limit. Sends limit and actual. |
media_url_invalid | 422 | Not an https URL. |
media_url_blocked | 422 | That address is private, loopback or link-local. |
media_url_unreachable | 422 | The host did not answer, redirected too many times, or answered with an error. When the host sent a short error sentence, the message carries it (for example Wikimedia's "Use thumbnail sizes listed on https://w.wiki/GHai"). A Wikimedia thumbnail at a width Wikimedia does not serve is fetched once more at the largest standard width below it, and a 429 with a Retry-After of a few seconds is retried once after waiting. When the connection does not open or no response comes back, and the host has more than one address, the fetch is tried once more on the next address. |
media_unreachable | 503 | The channel could not fetch the attached media from its URL at that moment (Telegram: "Wrong file identifier/HTTP URL specified"). The adapter already retried with a cache-busting URL before giving up. Recorded on the target's errorCode, not returned from a request. Retry the post; if it keeps failing, upload the file again. |
workspace_required | 400 | This account has more than one workspace and the request did not say which. Sends workspaces, each with id, name, slug and channels, so you can match what the person said without extra calls. Pass the x-uplika-workspace header, or the workspaceId argument on MCP tools. |
workspace_mismatch | 404 | The post, channel, or media is in a different workspace on the same account. Sends workspaces. Retry there instead of connecting the account again. |
workspace_limit | 403 | The plan includes fewer workspaces than this account already has. Sends used and limit. Workspaces are the billing unit, so the fix is a larger plan, not a retry. |
platform_already_connected | 409 | This workspace already has a channel on that platform, and a workspace holds one per platform. Sends platform. Disconnect the existing one or use another workspace. Reconnecting the same account is not affected. |
quota_exceeded | 402 | The free plan allows a fixed number of posts a month and this workspace used them all. Publishing is blocked until the next period starts, which is a month from the signup date, not the first of the calendar month. Upgrade to remove the limit. A thread counts as one post, not one per item. |
workspaces_invalid | 422 | The checkout asked for fewer workspaces than a paid plan includes, or for something that is not a whole number. Send the total number of workspaces you want, not the number of extra ones. |
billing_not_configured | 503 | Billing is not switched on for this deployment. Nothing to buy yet. |
full_access_open | 409 | Every feature is currently open to all users, so there is nothing to buy. Try again once plans are switched on. |
checkout_failed | 503 | The payment provider did not return a checkout. Nothing was charged. Try again. |
no_subscription | 422 | There is no paid subscription on this account, so there is no billing portal to open. |
portal_failed | 503 | The payment provider did not return a billing portal link. Try again. |
media_not_uploaded | 404 | media_complete was never called. |
media_expired | 404 | The upload was reclaimed. Upload the file again. Also set as a post target's errorCode when the attached files were already gone at the moment it went out; that target cannot be retried. |
media_not_publishable | 422 | mediaIds names a document (PDF, HWP or HWPX). Documents are taken for DM automations only (deliver.mediaId); a post carries photos and videos. Sends mediaIds. Remove them and send again. |
media_in_use | 409 | DELETE on a media item a post still uses, or on an original that a copy in another workspace still shares. Sends postId or cloneId. Delete that first. |
media_delete_failed | 503 | Storage did not delete the file. The row is kept so you can retry; nothing else changed. |
media_store_failed | 503 | media_from_url fetched the file but storage refused it. Nothing was saved. Try again. |
upload_bytes_missing | 409 | presign is refused because earlier presigned uploads never received their bytes. Sends stale, staleAfterMinutes and nextStep (media_upload_link). presign only works if you can PUT the file yourself; use media_upload_link for a file on someone's device, or media_from_url for a public https address. Over MCP the tool result also carries a ready upload link (uploadLink) to give to the person, and the same link comes back while it lives. |
api_unreachable | 503 | An MCP tool called our REST API and got no JSON back, such as a gateway page or a dropped connection. For a read, nothing was changed; try again. If a write (publish, edit, delete) ran out of time, it may still be running: the message says what to check (get_post, list_posts or list_naver_drafts) before sending it again. |
naver_media_kind_mismatch | 422 | The Naver Blog body refers to media with the wrong syntax: a video written as a photo (), a photo written as @video(media:ID), or a video inside @group(). Checked on content, each threadItems entry and options.naver_blog.content, on create and on edit. The response lists each mismatch with body, line, mediaId, kind and the line to write instead. Photos use ; videos use @video(media:ID) on their own line. |
naver_media_over_limit | 422 | The Naver Blog body places more photos or videos than Naver takes per post, on its own or together with the attached media that come before it, so some references would not go out. Checked on create and on edit, before anything is saved. Sends referenced (images, videos), limits (images, videos) and droppedIds, the references that would not go out. Nothing was changed. Place fewer media in the body, or pass mediaIds with only the media the body places. |
media_ref_unknown | 400 | The Naver Blog body points at media:<id> we cannot find. The response names the ids. Check them against what media_presign or media_from_url returned; if an id is right, the upload was reclaimed and the file has to go up again. Retrying the same payload will not help. |
media_ref_not_supported | 422 | The body places media with media: references, which only Naver Blog understands, but a target on another channel would publish that markup as literal text. Publish to Naver Blog on its own, or drop the references and pass mediaIds. |
media_not_configured | 503 | Media storage is not set up. |
post_not_found | 404 | No such publish in this workspace. DELETE also answers this for a post whose publish record was removed when its account was disconnected before 7 October 2026; then the message says the post is still on the channel and to call delete_post with the channel link. |
media_not_found | 404 | No such media in this workspace. |
key_not_found | 404 | No such API key. |
grant_not_found | 404 | No such authorization. |
nothing_to_retry | 422 | Every target already succeeded, or the post has no targets left because its account was disconnected before 7 October 2026 and its publish record was removed (the message says so and points to the channel link). |
not_published | 422 | The post has not gone out yet. |
delete_window_expired | 422 | Telegram only lets a bot delete messages for 48 hours after posting. Retrying will not help; the message has to be removed by hand in the app. |
platform_error | 503 | The channel refused or did not answer. |
duplicate_post | 409 | The same content went to this account within 10 minutes. Sends postId. Pass force: true to publish anyway. A retry answers it too, however long ago the post failed: when a different post with the same content and files is queued, going out, published or scheduled on the same account, and was not created more than 10 minutes before the one you retry. Then postId is that post and targets are the failed targets of this post that were held back. Nothing is retried; pass force: true to send it anyway. |
unauthenticated | 401 | No session and no valid key. |
platform_rejected_content | 400 · 404 · 422 · 503 | The channel refused the content itself. Retrying will not help. Read paths pass the upstream status through; the publish path reports it as 503. |
platform_auth_error | 503 | The channel refused our access to that account. Reconnect it. Kept at 503 so it is never confused with a problem with your uplika key. |
topic_tag_invalid | 422 | The topic tag has a period or an ampersand in it. Threads does not allow those. |
youtube_video_not_replaceable | target | An update to a published YouTube video came with a new video file. YouTube does not swap the file of a video that is already up; edit the title, description, tags or privacy instead, or publish a new video. |
topic_tag_not_supported | 422 | One of the channels you picked does not take a topic tag. Sends platform. Publish to it separately or drop the tag. |
api_key_revoked | 401 | That key was revoked. Create a new one. Kept separate from invalid_api_key so you do not go looking for a typo. |
api_key_expired | 401 | That key passed its expiry. Create a new one. |
idempotency_key_reused | 422 | That Idempotency-Key was already used with a different body. Use a new key for a new request. |
publish_in_flight | 409 | That post is still going out. Deleting mid-flight would let it publish after you deleted it. Wait a few seconds and try again. |
extension_outdated | 422 | The uplika browser extension in the user's Chrome is too old for this action (for example update_post needs 0.4.0 or later, a Naver Blog post whose photos carry the AI usage label needs 0.7.3, and rewriting a Naver post written in Naver's editor needs 0.7.8). Nothing was sent. The response carries extensionVersion and requiredVersion. Uplika asks that Chrome to update the extension at the same moment, and with no Naver job running it switches on its own, so call again in a minute or two; if it is still the old version, ask the user to press Update now on the extension badge at the top of the dashboard. If the required version is not on the Chrome Web Store yet (still in review), the message says so and bridge.awaitingRelease is true: updating finds nothing, Chrome installs it on its own once it is published, and until then the action has to be done by hand in Naver. |
thread_and_content | 422 | You passed both content and threadItems. Pass one. |
thread_empty | 422 | threadItems was an empty array. |
container_expired | 503 | The upload container expired before the post went out. Create the post again. |
permission_denied | 503 | The channel refused for permission reasons. |
invalid_json | 400 | The request body could not be read as JSON: broken syntax, a top level that is not an object or array, a charset other than UTF-8, or a Content-Encoding other than none, gzip, deflate or br. The message says which, in brackets. Nothing was run; fix the body and send it again. This call is not in /v1/logs because it stopped before we knew who sent it; quote the x-request-id response header if you contact us. |
body_too_large | 413 | The request body is larger than this path accepts (100KB for REST and MCP). Sends limit in bytes. Nothing was run; send a smaller body. |
internal_error | 500 | Our fault. The response carries no detail; the request id is in the logs. |
system_error | 503 | A target failed on our side, not the channel's — an adapter or the runner threw before the post went out. Recorded on the post's error, not returned from a request. Try again. |
rate_limited | 429 | Over the per-minute limit. Sends limit and retryAfter, and the response carries Retry-After. |
account_not_found | 404 | No such connected account in this workspace. |
account_has_pending_posts | 409 | DELETE /v1/accounts/:id found posts, automations or conversations on that account that disconnecting would delete. Sends pending with scheduledOnly (scheduled posts that go only to this account; disconnecting deletes them), scheduledShared (scheduled posts that also go to other accounts; they lose only this one), drafts (drafts that lose only this account), earliestScheduledAt, liveAutomations (enabled automations that would be deleted) and conversations (inbox conversations that would be deleted with their messages). Nothing was changed. To reauthorize the same account and keep all of this, reconnect it instead. Send force: true in the body, or ?force=true, to disconnect anyway. |
account_disconnected | 409 | The account that published this post was disconnected. Reconnect the same account on Connections and the post id comes back. force does not override it. Edit, delete, retry, replies, hide, like and insights on that post answer this; nothing was changed. The post keeps its status, and its target shows connectionId null with accountHandle set to that account's handle. |
account_busy_publishing | 409 | A post is going out through that account right now. Sends retryAfterSeconds. force does not override it; wait for the publish to finish and disconnect again. |
empty_reply | 422 | The reply had no text. |
cannot_hide_own_reply | 400 | Threads does not let an account hide its own replies. Hiding works on other people's replies to your posts. |
media_fields_required | 422 | presign needs fileName, contentType and bytes. When only bytes is missing or 0, reason is bytes_unknown and nextStep is media_upload_link: a client that cannot read the file size cannot PUT it either. Over MCP the tool result then carries an upload link too. |
not_our_post | 422 | retry_post on a post that was imported from the channel, not published through us: it is already on the channel, so there is nothing to retry. Editing an imported post answers post_not_editable instead when the channel cannot rewrite it. |
key_name_required | 422 | Give the key a name. |
key_scope_required | 422 | A key must reach at least one workspace. Pass workspaceIds, or allWorkspaces. |
invalid_locale | 422 | locale must be en or ko. |
url_required | 422 | Pass url: a public Naver Blog post link. |
reference_invalid | 400 | That is not a link to one Naver Blog post. Use a form like https://blog.naver.com/someone/224349824125. |
reference_not_naver | 400 | Only blog.naver.com links can be read as a reference. We do not fetch arbitrary hosts. |
reference_not_public | 400 | That post is private or gone. Only public posts can be used as a reference. |
reference_not_readable | 400 | That page is not a SmartEditor post. Posts from older editors cannot be read. |
reference_too_large | 400 | That post is too large to read. |
reference_timeout | 503 | Naver did not answer in time. Try again. |
reference_unreachable | 503 | Could not reach that post. |
reference_not_read | 422 · 404 | That reference post has not been read yet. Read it with POST /v1/naver/forms/extract first; the saved form is what generation uses. |
bad_post_ref | 422 | The post reference was not an id, a media id or a post link. |
post_not_owned | 422 | That post belongs to an account this workspace has not connected. |
post_not_in_recent | 422 | That post was not in the recent window we searched. |
upload_session_not_found | 404 | That upload link is gone or expired. |
upload_session_full | 422 | That upload link already has its maximum files. |
media_url_invalid | 422 | Not an https URL. |
media_url_blocked | 422 | That address is private, loopback or link-local. |
media_url_unreachable | 422 | The host did not answer, redirected too many times, or answered with an error. When the host sent a short error sentence, the message carries it (for example Wikimedia's "Use thumbnail sizes listed on https://w.wiki/GHai"). A Wikimedia thumbnail at a width Wikimedia does not serve is fetched once more at the largest standard width below it, and a 429 with a Retry-After of a few seconds is retried once after waiting. When the connection does not open or no response comes back, and the host has more than one address, the fetch is tried once more on the next address. |
workspace_required | 400 | This account has more than one workspace and the request did not say which. Sends workspaces, each with id, name, slug and channels, so you can match what the person said without extra calls. Pass the x-uplika-workspace header, or the workspaceId argument on MCP tools. |
workspace_mismatch | 404 | The post, channel, or media is in a different workspace on the same account. Sends workspaces. Retry there instead of connecting the account again. |
workspace_limit | 403 | The plan includes fewer workspaces than this account already has. Sends used and limit. Workspaces are the billing unit, so the fix is a larger plan, not a retry. |
platform_already_connected | 409 | This workspace already has a channel on that platform, and a workspace holds one per platform. Sends platform. Disconnect the existing one or use another workspace. Reconnecting the same account is not affected. |
quota_exceeded | 402 | The free plan allows a fixed number of posts a month and this workspace used them all. Publishing is blocked until the next period starts, which is a month from the signup date, not the first of the calendar month. Upgrade to remove the limit. A thread counts as one post, not one per item. |
workspaces_invalid | 422 | The checkout asked for fewer workspaces than a paid plan includes, or for something that is not a whole number. Send the total number of workspaces you want, not the number of extra ones. |
billing_not_configured | 503 | Billing is not switched on for this deployment. Nothing to buy yet. |
full_access_open | 409 | Every feature is currently open to all users, so there is nothing to buy. Try again once plans are switched on. |
checkout_failed | 503 | The payment provider did not return a checkout. Nothing was charged. Try again. |
no_subscription | 422 | There is no paid subscription on this account, so there is no billing portal to open. |
portal_failed | 503 | The payment provider did not return a billing portal link. Try again. |
youtube_title_required | 422 | A YouTube target needs options.youtube.title. |
youtube_title_too_long | 422 | The title is over 100 characters. Sends limit and actual. |
youtube_angle_brackets | 422 | YouTube refuses < and > in the title and the description. |
youtube_video_required | 422 | YouTube takes exactly one video. Sends limit and actual. |
youtube_tags_too_long | 422 | Tags are over 500 characters in total. A tag containing a space costs two extra for the quotes YouTube adds. Sends limit and actual. |
youtube_schedule_needs_private | 422 | options.youtube.publishAt only works while privacyStatus is private. |
youtube_schedule_invalid | 422 | options.youtube.publishAt was not an ISO 8601 timestamp. |
youtube_thumbnail_type | 422 | A thumbnail must be image/jpeg or image/png. |
youtube_thumbnail_too_large | 422 | A thumbnail can be up to 2MB. Sends limit and actual. |
youtube_short_no_thumbnail | 422 | A vertical video of three minutes or less publishes as a Short, and Shorts do not take custom thumbnails. Drop the thumbnail or change the video. |
youtube_no_threads | 422 | threadItems with a YouTube account would upload one video per item. Publish the video on its own. |
instagram_reel_video_required | 422 | An Instagram reel needs exactly one video and nothing else. Sends limit and actual. |
instagram_reel_duration | 422 | The reel is outside 3-900 seconds. Sends limit and actual. |
instagram_story_single_media | 422 | An Instagram story takes exactly one image or video. Sends limit and actual. |
instagram_story_no_extras | 422 | Stories do not take a first comment or collaborators. |
instagram_story_duration | 422 | The story video is outside 3-60 seconds. Sends limit and actual. |
instagram_cover_reel_only | 422 | options.instagram.coverMediaId only applies to a reel. |
instagram_cover_type | 422 | A reel cover must be image/jpeg. |
instagram_cover_too_large | 422 | A reel cover can be up to 8MB. Sends limit and actual. |
instagram_too_many_collaborators | 422 | Instagram takes up to 3 collaborators. Sends limit and actual. |
instagram_user_tag_position | 422 | A user tag on a photo needs x and y between 0 and 1. |
facebook_link_with_media | 422 | A Facebook link post cannot carry media. Drop the link or the files. |
facebook_link_invalid | 422 | options.facebook.link must be an http(s) URL. |
facebook_reel_video_required | 422 | A Facebook reel needs exactly one video and nothing else. Sends limit and actual. |
facebook_reel_duration | 422 | The reel is outside 3-90 seconds. Sends limit and actual. |
facebook_story_single_media | 422 | A Facebook story takes exactly one image or video. Sends limit and actual. |
facebook_story_no_extras | 422 | Facebook stories do not take a first comment. |
facebook_story_no_caption | 422 | Facebook stories do not show a caption. Drop the text, or publish as a feed post. |
facebook_schedule_feed_only | 422 | Scheduling works on Facebook feed posts only, not reels or stories. |
facebook_schedule_invalid | 422 | options.facebook.scheduledPublishTime was not an ISO 8601 timestamp. |
facebook_schedule_range | 422 | A scheduled Facebook post must be 10 minutes to 28 days from now. |
facebook_targeting_text_only | 422 | options.facebook.feedTargeting works on Facebook text and link posts only. Facebook takes it on photo posts and reels but does not keep it. |
facebook_targeting_not_editable | 422 | options.facebook.feedTargeting cannot be changed after the post is published. Leave it out to edit the text, or delete the post and publish it again. |
facebook_schedule_no_first_comment | 422 | A scheduled Facebook post cannot carry a first comment — the post is not live yet, so the comment has nowhere to attach and we are not called back at publish time. Publish now, or add the comment after it goes live. |
threads_not_supported | 422 | threadItems aimed at a channel that does not chain (Instagram, Facebook, Telegram, TikTok, Naver Blog): replies there are comments or do not exist. Only Threads and Bluesky take a chain. Publish one post, or send threadItems to those separately. |
beta_access_required | 403 | That channel is in beta and this workspace has not been approved for it yet. Request access from the dashboard. Retrying will not help. |
naver_title_required | 422 | Naver Blog needs options.naver_blog.title. |
naver_title_too_long | 422 | Naver Blog title over the limit. Sends limit and actual. |
naver_category_required | 422 | Naver Blog needs options.naver_blog.categoryId or options.naver_blog.category (the name). list_accounts returns the blog's categories under naverBlog.categories; there is no default. Not required when draftOnly is true. |
naver_markdown_unknown | 422 | The Naver Blog body uses a directive or {.attribute} we do not know. The message names the line and lists what is known. Nothing was published. |
naver_category_unknown | 422 | options.naver_blog.category names a category the connected blog does not have. The response lists the blog's categories; pick one of those or call refresh_account if it was just created. |
naver_category_ambiguous | 422 | options.naver_blog.category matches more than one category of the connected blog (a parent and a subcategory can have the same name). Nothing was published. The response lists the blog's categories with their ids; pass options.naver_blog.categoryId instead. |
naver_form_unknown | 422 | options.naver_blog.form (or the forms argument of /v1/naver/layout) names a preset that does not exist. The message lists the ids; GET /v1/naver/forms describes them. |
naver_form_required | 400 | Publishing to Naver Blog when nobody has picked a form yet. The error carries forms (id, name, when): show them, then publish again with options.naver_blog.form set, or pass options.naver_blog.layout: "as-is" to publish the markdown unchanged. The choice is remembered per workspace, so this is asked once. |
content_required | 422 | POST /v1/naver/layout without content. Pass the markdown body. |
help_not_found | 404 | GET /v1/help with an id that is not a question. Call it without arguments for the list. |
invalid_locale | 400 | GET /v1/help with a locale other than en or ko. |
invalid_category | 400 | GET /v1/help with a category it does not have. The message lists them. |
invalid_topic | 400 | GET /v1/naver/grammar with a topic we do not know. The message lists the ids. |
naver_wrong_blog | 400 | The Naver Blog link points at a different blog than the one connected here. We only touch posts on the connected blog. |
naver_wrong_login | 400 | The browser running the extension was signed in to a different Naver account than the blog this post was for, so nothing was written to Naver. Log in with the account that owns that blog. When the job is sent again and Naver asks for a login, the extension opens the Naver login window and names the account it needs. |
naver_session_permission | 400 | Multi-account switching is on in the extension but its cookies permission was revoked. Open the uplika extension popup in that browser and turn it on again. |
naver_rate_limited | 429 | Naver is limiting how often this blog can publish. This is Naver's own per-account limit, not ours, and it clears with time. The post may have been kept as a draft in Naver (a scheduled post that was due while the cap was still on is not sent at all and leaves no draft), so check there before publishing again later. |
naver_not_allowed | 400 | Naver does not accept that action from this account. Liking your own post is the known case: the click is silently ignored, so we report it instead of pretending. Nothing was changed. |
naver_follow_blocked | 400 | That blog does not accept neighbor requests, or Naver refused this one. Trying again does the same thing. |
naver_comment_gone | 404 | The comment you are replying to or liking is no longer on the post; it was deleted. Trying again does the same thing. Nothing was written to Naver. |
upload_stalled | 503 | uplika could not finish sending the video to the channel: a YouTube resumable upload stopped moving for 30 minutes, or ran past 6 hours. Nothing was published. This is on our side, so retry_post is safe, and the video file is kept for 7 days. Recorded on the target's errorCode, not returned from a request. |
platform_timeout | 503 | The channel did not answer in time: a container that never became ready, or a connection to the channel that never opened. Nothing was published. Try again. Recorded on the target's errorCode, not returned from a request. |
naver_draft_unpublished | 400 | The post is a Naver draft (temp save). It has no views or comments until it is published, and it cannot be edited or deleted as a post. Publish it with publish_naver_draft (POST /v1/naver/drafts/{logNo}/publish). |
naver_stats_unavailable | 400 | Naver's view stats have nothing for this post: it may be private, just published, or not on this blog. Trying again does the same thing; the other metrics and the stored history still come back. |
naver_comments_closed | 400 | The Naver post is private or has comments turned off, so there are no comments to read or reply to. This is the post's state, not a failure. |
naver_post_unavailable | 404 | Naver did not open that number as a post: it was deleted, is a draft, is scheduled, or belongs to another blog. Nothing was deleted and our record is unchanged. A record imported from that number in the same call is removed. |
naver_card_unavailable | 400 | The link card window did not open in the Naver editor, or putting the card in did not finish in time (the preview was slow or the page stopped answering). Sending the post again usually works. To post without a card, put the address on its own line instead of @card(…). A half-finished Naver draft may have been left. |
naver_card_not_inserted | 400 | Naver took the address in the link card window but did not make a card from it; YouTube video addresses do this because Naver puts a video embed there instead. Nothing went out and sending it again gives the same result, so retryable is false: write the address on its own line or as [text](address) instead of @card(…). A half-finished Naver draft may have been left; list_naver_drafts shows it. Uplika already sends @card with a YouTube video address as a plain link and says so in the target warning. |
naver_tab_lost | 400 | The Naver window closed, moved to another page or stopped answering before the publish button, or the editor did not load in time. Nothing went out (an edit leaves the post unchanged), so sending it again is safe. If it stopped while photos, videos, link cards or blocks were being put in, a half-finished Naver draft may have been left; list_naver_drafts shows it. For a reply it stopped before the reply was written, so no reply went out. |
naver_draft_unknown | 409 | The extension went quiet right after pressing Save as draft (the window closed, the page stopped answering, or the browser went away), so the draft may or may not be in Naver's draft list. Uplika looks in Naver's draft list first and recovers the draft when it is there; this code is what is left when the draft was not found or the list could not be read. Recorded on the target's errorCode with retryable false; retry_post answers 409 with this code until the call carries force: true. Check list_naver_drafts first; retrying without checking can leave two drafts. |
naver_reply_unknown | 400 | A Naver reply step did not confirm in time (the reply command timed out, the page stopped answering, or the reply did not show up after it was submitted), so the reply may already be under the comment. Look at that comment on the blog first (a secret reply shows only to the blog owner's login; list_replies may not show nested or secret replies). On a reply to our own post it stays on the target's errorCode with retryable false, and retry_post answers 409 with this code until the call carries force: true. On a reply to someone else's post, an inbox answer or an automation reply it comes back as 400. Sending it again without checking can post the reply twice. |
naver_publish_unconfirmed | target | The publish step in Naver did not confirm in time (the publish or confirm command timed out, or the page stopped answering after the confirm button may have been pressed), so whether the post went out is not known. For an edit, open the post; if it did not change, call update_post once more; sending an edit again does not make a second post. For a new post Uplika has already checked the blog's post list and the post is not there; a half-finished Naver draft may have been left, so check list_naver_drafts and send it again. retryable is true. A timeout while the publish panel was still being prepared is naver_publish_failed, not this code. |
comments_closed | 400 | Comments are turned off on this post (for example a YouTube video with comments disabled). There is nothing to read or reply to. This is the post's state, not a failure; the account does not need reconnecting. |
refresh_unsupported | 400 | This channel has nothing to refresh; its metadata comes from the platform on every call. |
invalid_params | 400 | A request field is missing or the wrong shape. The message names it; problems lists each field when there are several. |
quota_exceeded | 429 | A daily research quota is used up: 30 judge calls, 300 measured keywords or 20 blog diagnoses per person. Sends what (seeds, measured, diagnoses) and limit. Cached keywords and cached diagnoses do not count, and measurements that never reached Naver are refunded. Keyword expansion has no daily quota. |
naver_seo_unavailable | 503 | Keyword research is not configured on this server (no Search Ad or API HUB keys). Type keywords by hand; the SEO check still works. |
naver_api_error | 503 | Naver Search Ad or API HUB did not answer or refused our credentials. Try again later. |
invalid_blog_id | 400 | blog is missing or is not a Naver blog id: 2-40 letters, digits, - or _, or a link to blog.naver.com that carries one. Nothing was fetched. |
blog_not_found | 404 | No public Naver blog has that id, or it has no public posts. Naver answers an empty feed for unknown ids, so this is the same as a redirect away from the feed. |
naver_rss_unreachable | 503 | The blog's RSS feed did not come back whole: Naver did not answer in 12 seconds, answered an error, or the feed was over 1 MB. Try again later; nothing is cached from a failed read. |
naver_autocomplete_unavailable | 503 | Neither path could run: the person's uplika Chrome extension is not on (or cannot reach Naver autocomplete yet), and server-side autocomplete is switched off. Nothing was sent to Naver. Pass your own keyword list to judge or research_naver_keywords instead. |
naver_autocomplete_paused | 429 | Naver started refusing our autocomplete calls, so the server is pausing them for everyone (5 minutes, doubling on repeat, up to an hour). Sends retryAfterSeconds and a Retry-After header. Pass your own keyword list meanwhile. |
naver_autocomplete_busy | 429 | Autocomplete cannot take this call right now: the queue is too deep, an expansion is already running for you, our server already ran one for you in the last 60 seconds, our daily cap is used up, or your recent seeds all came back empty. The message says which. Sends retryAfterSeconds and a Retry-After header. The 60-second spacing applies only when our server answers; the person's uplika Chrome extension has none. |
research_tool_disabled | 403 | The person has not turned this research tool on for MCP connectors and API keys (it is off by default; the screen always works). Sends tool and enableUrl, the research screen with its details sheet open. Tell the person to open that link and turn it on, then say so; retrying before that returns the same error. |
research_quota_exceeded | 429 | A daily research quota is used up. Sends kind (youtube_searches) and limit. Cached searches do not count; the quota resets at the UTC day change. |
youtube_quota_exhausted | 429 | The shared YouTube Data API quota for today is used up, either our own daily budget or Google's. Sends resetsAt. Cached searches still answer; new ones wait for midnight Pacific time. |
youtube_search_unavailable | 503 | YouTube search is not configured on this server (no YouTube Data API key). Nothing was sent to Google. |
naver_seo_busy | 429 | Our own daily cap for Naver Search Ad or API HUB calls is used up (it protects the shared credentials, separate from your personal quota). Sends service, retryAfterSeconds and a Retry-After header; the cap resets at the UTC day change. |
refresh_timeout | 202 | refresh_account is still waiting for the browser extension after a minute. Not an error: the refresh keeps running. Call list_accounts again shortly. |
refresh_failed | 503 | The browser extension could not re-read the blog. bridgeCode carries the extension's reason. Open blog.naver.com in that browser and try again. |
naver_reply_media_not_supported | 422 | A reply to a Naver Blog post carried media, either in mediaIds or through a media: reference in the body. Naver comments are written into the comment box on the post page and take text only, so the media would be dropped without a trace. Publish it as its own post instead. |
naver_too_many_tags | 422 | More than 30 tags. Sends limit and actual. |
naver_tag_too_long | 422 | A tag over 30 characters. Sends limit. |
naver_tag_has_space | 422 | Naver Blog tags cannot contain spaces. Join the words. |
naver_scheduled_at_conflict | 422 | scheduledAt on the post and options.naver_blog.scheduledAt name different times. They mean the same thing (Uplika holds the post and sends it then); pass one of them. |
naver_scheduled_at_mixed | 422 | options.naver_blog.scheduledAt on a post that also targets other channels. It now schedules the whole post on Uplika's side, so pass scheduledAt on the post to send every channel then, or publish the other channels separately. |
naver_draft_and_schedule | 422 | options.naver_blog.draftOnly and options.naver_blog.scheduledAt cannot both be set. A Naver draft is not scheduled. |
naver_font_size_out_of_range | 422 | options.naver_blog.fontSize must be 10-34. |
naver_ui_changed | target | The browser extension could not find or drive a control it expected in Naver's editor. Retrying will not help until the extension is updated. This fires from the pre-flight check when a control is really missing (an editor that was only slow to load is naver_tab_lost), which touches nothing, and also from mid-assembly, which can leave a half-built draft in Naver. A publish panel that closed before the confirm button was pressed is naver_publish_failed, not this code. Check the blog before assuming nothing happened. |
naver_not_logged_in | target | The Naver login for that blog's account in the Chrome with the extension was signed out. While that blog is held, its posts are not failed but wait, and bridge.state is login_needed. The person presses Open login window on the Connections page and logs in to that account in the window that opens. Reads, and posts that waited 7 days, close with this code. After the login is back, retry_post. |
naver_upload_failed | target | An image, video or link card did not go into the editor. The extension tries to leave a Naver draft behind, but it does not report whether that worked, so treat the draft as likely and not certain. Open the blog before retry_post. |
naver_publish_failed | target | The editor was filled but Naver did not publish. The extension tries to leave a draft; that is likely, not certain. Check the blog before retry_post so it is not posted twice. Also used when the extension stopped before Naver's publish panel was ready (the prepare or tag-cleanup command timed out, the page stopped answering, or the browser went away during it), or when the panel did not open after the publish button was pressed: then the confirm button was never pressed, an edit left the post unchanged, and sending it again is safe. The message says which. |
naver_verify_failed | target | The job came back done without a post id, so we cannot confirm anything went live. The target is marked failed. Open the blog and look before retrying: a post may exist even though we could not read it back. |
bridge_unsupported | target | The extension cannot handle this job, usually a file too large to hand to the editor. Retrying will not help. |
bridge_lost | target | The browser extension went away mid-job. A draft may exist in Naver. Check it before retry_post. When it went away during the publish step, Uplika checks the blog's post list first: a post found there is recorded as published, and this code means it was not there (the publish button may not have been pressed). |
bridge_stalled | target | The extension stayed connected but the job stopped making progress. A draft may exist in Naver. Check it before retry_post. |
bridge_timeout | target | No browser extension came online within 7 days, so the queued post expired. retry_post once the browser is open. |
naver_category_missing | target | The category chosen for this post (options.naver_blog.categoryId) is not on this blog, so the publish panel could not select it and nothing went out. Pick a category this blog has (list_accounts shows them) and send it again. retryable is false. |
naver_grammar_rejected | target | The extension's parser rejected one line of the Naver body (the message carries the line number and reason, for example an attribute this extension version does not know), so the post was not assembled and nothing went out. Fix that line and send it again; retry_post on its own does the same thing. retryable is false. |
platform_activity_blocked | target | Instagram has temporarily restricted this account's activity (error code 4 with subcode 2207051). Unlike a plain app rate limit, waiting alone does not clear it: the person checks the notice in the Instagram app, and a retry fails until the restriction is lifted. retryable is false. |
youtube_daily_cap | target | A scheduled YouTube post was due while uplika's shared daily upload cap was already used up, so it was not started. The cap resets at midnight Pacific time; retry_post after that. retryable is true. |
bridge_offline | 503 | A read or delete on Naver Blog while the browser extension is offline. Nothing was queued; try again when the browser with the extension is open. The error detail carries bridge.wake: per-OS commands to open Chrome in the profile that has the extension, and a hint on what to call next. |
account_required | 400 | GET /v1/channels/publish-options was called without accountId. These values are per connected account, so there is no account-wide answer. |
not_supported | 400 | The channel cannot do what you asked, and that is a fact about the channel rather than a failure to retry. Read GET /v1/platforms: each channel carries features, and a row with supported false says so ahead of time and carries the same sentence you get here. It comes back from publish-options (only TikTok has anything to look up), replying to or reading the replies of a post on a channel whose API has no replies, and liking or following on a channel where uplika does not offer it. A row with supported partial is refused the same way when the account does not meet its condition: liking from an Instagram account that was not connected through Facebook comes back with this code before the channel is called, and the message is that row's reason. |
reconsent_required | 409 | The connection was made before it was granted the permission this call needs, and connecting again can grant it. Sends accountId and missingScopes. Reconnect that account from the Connections page, then try again. POST /v1/engagement/like answers it for an Instagram account connected through Facebook that lacks the like permission; while that permission cannot be granted yet, the same call answers feature_in_review instead. |
feature_in_review | 400 | The call needs a Meta permission that is still in app review, and this connection does not hold it. Reconnecting does not help until the review passes, so do not retry and do not send the person to reconnect; everything else on the account keeps working. Sends feature (like or mentions), accountId and missingScopes. GET /v1/platforms shows the same thing ahead of time: the feature's row is supported partial with why app_review, and the message here starts with that row's reason. Accounts that already hold the permission are not refused. POST /v1/engagement/like answers it for an Instagram account connected through Facebook; once the permission can be granted the same call answers reconsent_required instead. |
tiktok_privacy_required | target | options.tiktok.privacyLevel was missing. TikTok requires the person to choose visibility deliberately, so we have no default. This one is returned synchronously, before the post is created, so an agent can fix the call and send it again in the same turn. Call get_publish_options for the values this account may use. |
tiktok_privacy_invalid | target | options.tiktok.privacyLevel is not one of the values this account may use right now. The allowed list differs per account and changes with the person's TikTok settings, so read it from get_publish_options rather than hard-coding. |
tiktok_branded_private | target | brandContentToggle was set together with privacyLevel SELF_ONLY. TikTok does not allow branded content to be posted privately. |
tiktok_interaction_disabled | target | disableComment, disableDuet or disableStitch was set to false for something the creator turned off on their TikTok account. A post cannot re-enable it. |
tiktok_title_required | target | A TikTok photo post needs options.tiktok.title, up to 90 characters. A video post has no title field and its text comes from content. |
tiktok_title_too_long | target | options.tiktok.title is longer than 90 characters. TikTok counts a title in UTF-16 units, so an emoji outside the basic plane counts as two. |
tiktok_title_not_supported | target | options.tiktok.title was sent with a video. TikTok video posts have no title field, so the value would be dropped without a trace. The video's text comes from content. |
tiktok_cannot_post | target | TikTok says this creator cannot post right now, usually a posting limit or a restriction on the account. We stop before sending anything, which is what TikTok's guidelines require. |
tiktok_reply_not_supported | target | TikTok's public API has no way to reply to a post. Publishing a reply there would silently become a new post, so we refuse instead. |
내 키가 실제로 무엇을 보냈는지 어떻게 보나요
REST 와 MCP 호출이 요청·응답 본문과 함께 남습니다. 발행이 왜 거절됐는지 찾는 제일 빠른 길입니다. 채널이 한 말이 거기 그대로 들어 있습니다. 대시보드에서 한 일은 쓰기(발행 · 수정 · 삭제 · 업로드 · 설정 저장)와 실패, 조사 도구 사용만 남습니다. 로그 화면은 기본으로 대시보드 성공 줄을 숨기고, 「대시보드 요청 포함」 을 켜면 같이 보입니다. API 키 원문 같은 한 번만 보이는 값은 가려서 남깁니다.
GET/v1/logs
최근 API·에이전트 호출. 최신 순, 본문 포함. 대시보드 요청은 켜면 보입니다.
페이지 넘기기
| 이름 | 타입 | 설명 |
|---|---|---|
requestId | query | One call by the id an error response gave you. |
outcome | query | failed for 4xx and 5xx, ok for the rest. Omit for everything. |
search | query | Matches the path or the request id. |
since | query | YYYY-MM-DD. Calls from that day onward. |
limit | query | 1-200, defaults to 50. |
before | query | A log id from a previous page's nextBefore. |
자주 묻는 것
에러는 어떤 모양으로 오나요?
code 와 영어 message 를 가진 error 객체입니다. message 에 끼워 넣은 값은 따로 필드로도 오기 때문에, 우리 문장을 파싱하는 대신 자기 언어로 문장을 다시 만들 수 있습니다.
메시지가 왜 영어만 있나요?
응답이 언어를 타면 같은 요청이 묻는 사람에 따라 다르게 답하는 셈이라서입니다. 화면은 두 언어지만 API 는 아닙니다. message 가 아니라 code 로 그리세요.
rate_limited 를 받으면 어떻게 하나요?
기다렸다 다시 부르면 됩니다. 응답에 걸린 제한이 실려 오기 때문에 백오프를 짐작하는 대신 속도를 맞출 수 있습니다.