문서

에러

에러는 항상 같은 모양으로 와요. 코드는 구체적이고, 숫자 값은 따로 필드로 와요.

포샌디 API는 모든 에러를 같은 모양으로 보내요. 코드는 무엇이 잘못됐는지 구체적으로 알려 주고, 문장에 들어간 숫자 값은 따로 필드로도 보내요.

모양

실패하면 error 객체가 와요. 그 안에 code와 영어 문장 message가 있어요. message에 들어간 값(채널, 한도, 실제 값 등)은 별도 필드로도 와요. 이 값으로 한국어 안내 문장을 직접 만들 수 있어요.

{
  "error": {
    "code": "content_too_long",
    "message": "Threads allows 500 characters. This is 620.",
    "platform": "threads",
    "limit": 500,
    "actual": 620
  }
}

위 예시는 422 응답이에요.

같은 요청이 두 번 가지 않게 막는 방법

중복 발행은 두 단계로 막아요.

  1. Idempotency-Key 헤더: 이 헤더를 넣으면 같은 요청을 다시 실행하지 않고 저장해 둔 응답을 그대로 돌려줘요. 성공한 결과는 24시간 보관해요. 같은 키로 다른 본문을 보내면 idempotency_key_reused로 거절해요.
  2. 내용 비교: 같은 글과 같은 파일이 같은 계정으로 10분 안에 다시 오면 duplicate_post로 거절해요. 실패한 글을 재시도할 때도 같은 비교를 해요. 10분이 지났더라도 같은 계정에 같은 내용의 다른 글이 있으면 duplicate_post로 거절해요.

일부러 같은 글을 다시 올리는 거라면 force: true를 넣어요.

위반이 여러 개면 한 번에 와요

규칙을 두 개 이상 어기면 error.errors에 전부 담겨 와요. 첫 번째 항목은 error 자리에도 그대로 들어 있어요. 그래서 한 번에 다 고칠 수 있어요. 재시도할 때마다 다음 위반을 하나씩 확인하지 않아도 돼요.

예외는 두 개예요. no_accounts와 empty_content는 다른 검사를 하기 위한 전제 조건이라 혼자 와요.

메시지는 항상 영어예요

화면은 한국어와 영어를 지원하지만, API 메시지는 영어로만 와요. 응답이 언어마다 달라지면, 같은 요청에 묻는 사람마다 다른 답이 오게 되기 때문이에요. 화면에 보여 줄 문장은 message가 아니라 code를 기준으로 만들어요.

코드

코드상태뜻과 할 일
verification_required401이메일 확인을 아직 안 끝냈어요.
next_post_taken409이 계정에서 다음 글을 기다리는 자동화가 이미 켜져 있어요. 하나만 둘 수 있고, 다음 글이 그 자동화에 묶이면 자리가 비어요.
past_comments_not_supported400지난 댓글에 보내기는 Instagram과 Facebook에서만 돼요. 이 채널은 댓글을 다시 읽어 올 수 없어요.
past_comments_no_trigger400자동화에 댓글 트리거가 없어서 반응할 댓글이 없어요.
automation_not_live409자동화가 초안이에요. 먼저 켠 다음 지난 댓글에 보내요.
past_comments_no_post409자동화가 다음 글을 기다리는 중이라 아직 댓글을 읽을 글이 없어요.
past_comments_running409이미 지난 댓글에 보내는 중이에요. 끝날 때까지 기다리거나 DELETE로 멈춰요. 돌고 있는 작업은 pastComments에 있어요.
scheduled_at_invalid422scheduledAt이 ISO 8601 형식이 아니거나 시간대 오프셋이 없어요.
scheduled_at_range422scheduledAt이 지금부터 10분보다 가깝거나 1년보다 멀어요.
draft_and_schedule422draft와 scheduledAt을 같이 보냈어요. 하나만 골라요.
batch_id_invalid422batchId가 40자를 넘거나, 영문, 숫자, -, _ 말고 다른 글자가 있어요.
naver_source_required409원본이 네이버에만 있는 네이버 블로그 글을 update_post로 고치면서 현재 원본의 sourceVersion을 안 보냈거나, 원본을 읽은 뒤 네이버에서 글이 바뀌었어요. 옛 사본으로 만든 본문은 사진, 영상, 카드, 표, 링크, 서식을 잃어요. 응답에 source(@keep 줄이 들어간 현재 본문), keep(그 id들), sourceVersion이 와요. source를 고쳐서 sourceVersion과 함께 다시 보내요. @keep 줄은 그 블록을 지울 때만 빼요. 바뀐 것은 없어요.
naver_keep_unknown422@keep(...) id가 고치려는 글의 블록이 아니거나, 네이버 에디터로 쓴 글에서 update_post가 아닌 곳에 @keep을 썼어요. 현재 원본의 id를 써요(naver_source_required가 알려 줘요). 바뀐 것은 없어요.
post_not_editable409다시 쓸 수 없는 채널에 이미 나간 글에 PATCH나 /publish를 보냈거나, 아직 안 나간 글에 재시도를 보냈어요. 메시지에 list_platforms의 채널 사유(update 기능, 채널에서 직접 쓴 글이면 update_imported)가 와요. 모든 대상이 실패한 글은 나간 적이 없다고 알려 주고 retry_post를 안내해요. 예약 글과 초안은 언제든 고칠 수 있어요.
live_post_no_schedule422이미 나간 글에 scheduledAt이나 draft를 넣어 PATCH했어요. 나간 글에는 예약이나 초안 상태가 없어요. content, mediaIds, options로 고쳐요.
live_post_accounts_fixed422이미 나간 글에 다른 accountIds를 넣어 PATCH했어요. 나간 글의 채널은 바꿀 수 없어요. 같은 id를 넣거나 빼고 보내요.
nothing_to_change422이미 나간 글에 content, mediaIds, options 중 아무것도 없이 PATCH했어요. 바꿀 내용이 없어요.
target_required422좋아요, 답글, 팔로우 요청에 대상이 없어요. postId를 넣거나 ownerHandle과 externalId를 넣어요. 팔로우는 ownerHandle을 받아요.
thread_piece409이어진 글(스레드)의 두 번째 이후 항목에 PATCH, /publish, DELETE를 보냈어요. 응답에 rootId가 와요. 첫 항목에 보내요.
naver_draft_not_found404publish_naver_draft에 넣은 logNo가 블로그 임시저장함에 없어요. list_naver_drafts로 목록을 확인해요.
account_required422워크스페이스에 그 채널 계정이 두 개 이상 있어요. 응답에 accounts가 와요. accountId를 넣어요.
invalid_api_key401bearer 토큰이 올바른 키가 아니에요.
forbidden_scope403키에 필요한 권한이 없어요. 응답에 scope가 와요.
no_accounts422accountIds가 비어 있어요. 응답에 candidates가 오고, 연결된 채널이 없으면 next도 와요. 채널이 없는 게 아니라 만료된 경우에는 needsReconnect와 다시 연결하라는 안내가 와요.
empty_content422글도 파일도 없어요.
account_not_connected404accountIds 중 하나가 연결돼 있지 않아요. 응답에 candidates가 와요.
account_expired422채널이 이 로그인을 끝냈어요(만료 또는 회수). 응답에 platform이 와요. POST /v1/accounts/{id}/refresh도 이 코드로 답해요. 새로고침으로는 로그인이 살아나지 않아요. Threads, Instagram, Facebook에서는 발행, 답글, 지표, 인박스, 삭제 요청도 이 코드로 답하고, 그 연결은 만료로 표시돼요. 연결 화면에서 계정을 다시 연결해요.
platform_not_available422아직 열리지 않은 채널이에요. 응답에 platform이 와요.
content_too_long422글이 채널 한도보다 길어요. 응답에 platform, limit, actual이 와요.
media_required422이 채널은 글만으로는 올릴 수 없어요. 응답에 platform이 와요.
media_mixed422지금은 발행에서 이 코드를 보내지 않아요. 사진과 영상을 섞을 수 없는 채널은 영상을 남기고 사진을 빼요. 무엇을 뺐는지는 대상의 warning에 나와요. 다른 경로로 섞인 파일이 TikTok에 닿으면 아직 이 코드로 답해요.
media_too_many422지금은 보내지 않아요. 채널의 사진, 영상, 캐러셀 한도를 넘으면 앞에서부터 한도만큼만 올리고 대상의 warning에 알려 줘요. 한도는 GET /v1/platforms에서 확인해요.
media_too_large422파일이 너무 커요. 응답에 limit, limitMb, actual이 와요. media_from_url로 가져오는 영상은 프리사인보다 한도가 낮아요. 더 큰 영상은 media_upload_link나 프리사인으로 올려요.
media_aspect_ratio422화면 비율이 맞지 않아요. 응답에 limit과 actual이 와요.
media_dimensions422영상 가로 크기가 한도보다 커요. 응답에 limit과 actual이 와요. 사진 가로 크기로는 이 코드가 오지 않아요.
media_type_not_supported422받지 않는 파일 형식이에요. 응답에 types가 와요.
media_too_long422영상 길이가 한도보다 길어요. 응답에 limit과 actual이 와요.
media_url_invalid422https 주소가 아니에요.
media_url_blocked422사설망, 루프백, 링크 로컬 주소예요.
media_url_unreachable422서버가 답하지 않았거나, 리다이렉트가 너무 많거나, 오류로 답했어요. 서버가 짧은 오류 문장을 보내면 메시지에 같이 와요. Wikimedia 썸네일을 지원하지 않는 너비로 요청하면 그보다 작은 가장 큰 표준 너비로 한 번 더 가져와요. 몇 초짜리 Retry-After가 붙은 429는 기다렸다가 한 번 더 시도해요. 연결이 안 열리거나 응답이 없고 서버 주소가 여러 개면 다음 주소로 한 번 더 시도해요.
media_unreachable503채널이 그 순간 파일 주소에서 파일을 가져오지 못했어요. 포샌디가 포기하기 전에 이미 다시 시도했어요. 요청 응답이 아니라 대상의 errorCode에 남아요. 파일은 7일 보관하니 글을 재시도해요. 계속 실패하면 파일을 다시 올려요.
workspace_required400계정에 워크스페이스가 두 개 이상인데 요청에 어느 것인지 없어요. 응답의 workspaces에 각 워크스페이스의 id, 이름, slug, 채널이 와요. x-posendy-workspace 헤더나 MCP 도구의 workspaceId로 정해요. 헤더에 넣은 워크스페이스가 이 계정에 없거나, API 키 범위 밖이거나, 지워졌을 때도 같은 코드가 와요. 이때는 메시지가 이유를 알려 주고 workspaces에 쓸 수 있는 목록이 와요.
workspace_mismatch404글, 채널, 파일이 같은 계정의 다른 워크스페이스에 있어요. 응답에 workspaces가 와요. 계정을 다시 연결하지 말고 그 워크스페이스로 다시 요청해요.
workspace_limit403지금 플랜에 포함된 워크스페이스 수보다 이미 더 많이 갖고 있어요. 응답에 used와 limit이 와요. 워크스페이스가 요금 단위라서 재시도가 아니라 플랜을 올려야 풀려요.
platform_already_connected409이 워크스페이스에 그 채널이 이미 연결돼 있어요. 워크스페이스 하나에는 채널마다 하나씩만 연결돼요. 응답에 platform이 와요. 기존 연결을 끊거나 다른 워크스페이스를 써요. 같은 계정을 다시 연결하는 것은 상관없어요.
quota_exceeded402플랜의 사용 한도를 넘었어요. 지금은 전부 무료라 이 오류가 나지 않아요. 정식 출시 후 플랜 한도를 넘으면 이 코드가 와요.
workspaces_invalid422결제할 때 유료 플랜에 포함된 수보다 적은 워크스페이스를 요청했거나, 정수가 아닌 값을 보냈어요. 추가할 개수가 아니라 원하는 전체 워크스페이스 수를 보내요.
billing_not_configured503이 환경에서는 결제가 켜져 있지 않아요. 아직 살 수 있는 것이 없어요.
full_access_open409지금은 모든 기능이 모두에게 열려 있어서 살 것이 없어요. 플랜이 열리면 다시 시도해요.
checkout_failed503결제 대행사가 결제 화면을 돌려주지 않았어요. 결제된 것은 없어요. 다시 시도해요.
no_subscription422유료 구독이 없어서 결제 관리 화면을 열 수 없어요.
portal_failed503결제 대행사가 결제 관리 링크를 돌려주지 않았어요. 다시 시도해요.
media_not_uploaded404media_complete를 부르지 않았어요. 파일이 없거나 비어 있으면(0바이트) media_complete도 이 코드로 422를 줘요. 기록은 남아 있으니 업로드를 끝내고 다시 불러요.
media_expired404업로드한 파일이 정리됐어요. 파일을 다시 올려요. 글이 나가는 순간 붙인 파일이 이미 없었을 때도 대상의 errorCode에 남아요. 이 대상은 재시도할 수 없어요.
image_unreadable422media_complete가 사진이 덜 올라갔거나 손상된 것을 발견하고 버렸어요(업로드가 중간에 끊긴 경우). 파일을 다시 올려요. 글이 나가는 순간 사진을 읽지 못했을 때도 대상의 errorCode에 남아요. 이 대상은 재시도할 수 없어요. 파일을 다시 올리고 새 글을 만들어요.
media_not_publishable422mediaIds에 문서(PDF, HWP, HWPX)가 들어 있어요. 문서는 DM 자동화(deliver.mediaId)에서만 받아요. 글에는 사진과 영상만 붙어요. 응답에 mediaIds가 와요. 빼고 다시 보내요.
media_in_use409아직 글에서 쓰는 파일이나, 다른 워크스페이스의 복사본이 함께 쓰는 원본에 DELETE를 보냈어요. 응답에 postId나 cloneId가 와요. 그쪽을 먼저 지워요.
media_delete_failed503저장소가 파일을 지우지 않았어요. 기록은 남아 있으니 다시 시도해요. 다른 것은 바뀌지 않았어요.
media_store_failed503media_from_url이 파일을 가져왔지만 저장소가 받지 않았어요. 저장된 것은 없어요. 다시 시도해요.
upload_bytes_missing409앞서 받은 프리사인 주소로 파일이 올라오지 않아서 프리사인을 거절했어요. 응답에 stale, staleAfterMinutes, nextStep(media_upload_link)이 와요. 프리사인은 직접 파일을 PUT할 수 있을 때만 써요. 다른 사람 기기에 있는 파일은 media_upload_link, 공개 https 주소는 media_from_url을 써요. MCP로 부르면 사람에게 전할 업로드 링크(uploadLink)가 결과에 같이 오고, 링크가 살아 있는 동안 같은 링크가 와요.
api_unreachable503MCP 도구가 포샌디 REST API를 불렀는데 JSON이 아닌 응답이 왔어요(게이트웨이 화면, 끊긴 연결 등). 읽기 요청이면 바뀐 것이 없으니 다시 시도해요. 쓰기 요청(발행, 수정, 삭제)이 시간 안에 안 끝났으면 아직 진행 중일 수 있어요. 다시 보내기 전에 메시지가 알려 주는 대로 get_post, list_posts, list_naver_drafts로 확인해요.
naver_media_kind_mismatch422네이버 블로그 본문에서 파일 종류와 문법이 맞지 않아요. 영상을 사진 문법으로 썼거나, 사진을 @video(media:ID)로 썼거나, @group() 안에 영상을 넣었어요. 만들 때와 고칠 때 content, 각 threadItems, options.naver_blog.content를 검사해요. 응답에 틀린 곳마다 body, line, mediaId, kind, 대신 쓸 줄이 와요. 영상은 @video(media:ID)를 한 줄에 따로 써요. 사진 문법은 네이버 블로그에 있어요.
naver_media_over_limit422네이버 블로그 본문에 넣은 사진이나 영상이 네이버의 글당 한도보다 많아요. 본문만으로 넘거나, 앞에 붙는 첨부 파일과 합쳐서 넘는 경우예요. 저장 전에 검사해요. 응답에 referenced, limits, droppedIds, total, unplaced가 와요. 바뀐 것은 없어요. 본문에 안 쓰는 첨부 파일을 빼거나, 본문에 넣는 파일을 줄이거나, mediaIds에 본문에 쓰는 파일만 넣어요.
naver_media_ref_unattached422네이버 블로그 본문이 글에 첨부되지 않은 파일을 가리켜요. 이대로면 확장 프로그램이 그 줄에서 멈춰요. 서버는 파일 참조로 안 읽는데 확장은 읽는 모양으로 사진 줄을 쓴 경우에 생겨요(예: 줄 끝에 br 태그를 붙인 경우). 응답에 mediaIds가 와요. 바뀐 것은 없어요. 그 id를 mediaIds에 넣거나, 사진을 한 줄에 하나씩 쓰고 중괄호 속성은 한 쌍만, 그 뒤에는 아무것도 쓰지 않아요.
media_ref_unknown400네이버 블로그 본문의 media: 참조 id를 찾을 수 없어요. 응답에 그 id가 와요. media_presign이나 media_from_url이 돌려준 값과 비교해요. 올린 파일은 글이 나가고 하루쯤 뒤에 정리돼요. 이미 나간 네이버 글의 사진은 현재 원본의 @keep 줄이니 그 줄은 그대로 두고, media:는 지금 올리는 사진에만 써요. 같은 내용으로 재시도해도 소용없어요.
media_ref_not_supported422본문에 media: 참조를 썼는데, 이 문법은 네이버 블로그와 Ghost만 알아들어요. 다른 채널에서는 글자 그대로 올라가요. 그 채널은 따로 올리거나, 참조를 빼고 mediaIds를 써요.
media_ref_mixed_channels422본문에 media: 참조를 썼는데 글이 네이버 블로그와 Ghost에 같이 가요. 두 채널은 참조를 읽는 규칙이 달라요. 채널마다 글을 따로 올려요.
ghost_title_required422Ghost(준비 중)에는 options.ghost.title이 필요해요.
ghost_title_too_long422Ghost 제목이 코드 포인트 기준 255자를 넘어요. 응답에 limit과 actual이 와요.
ghost_tag_invalid422Ghost 태그가 비었거나, 191자를 넘거나, 쉼표가 있거나, #로 시작해요(#로 시작하면 Ghost가 숨김 태그로 만들어요). 응답에 limit이 와요.
ghost_directive_not_supported422Ghost 본문에 네이버 블로그 문법(@group, @video, @card, 사진 뒤 중괄호)을 썼어요. Ghost에서는 글자 그대로 나와요. 응답에 line이 와요.
media_not_configured503파일 저장소가 설정돼 있지 않아요.
post_not_found404이 워크스페이스에 그런 글이 없어요. 계정 연결을 끊을 때 발행 기록이 지워진 글에 DELETE를 보내도 이 코드가 와요. 이때는 글이 채널에 아직 있으니 채널 링크로 delete_post를 부르라고 메시지가 알려 줘요.
media_not_found404이 워크스페이스에 그런 파일이 없어요.
key_not_found404그런 API 키가 없어요.
grant_not_found404그런 승인 기록이 없어요.
nothing_to_retry422모든 대상이 이미 성공했거나, 계정 연결을 끊으면서 발행 기록이 지워져 남은 대상이 없어요. 후자면 메시지가 채널 링크를 알려 줘요.
not_published422글이 아직 나가지 않았어요.
delete_window_expired422Telegram은 봇이 올린 메시지를 48시간 안에만 지울 수 있어요. 재시도해도 소용없어요. 앱에서 직접 지워요.
platform_error503채널이 거절했거나, 답하지 않았거나, 업로드 세션을 끝내기 전에 끊었어요.
duplicate_post409같은 내용이 10분 안에 이 계정으로 나갔어요. 응답에 postId가 와요. 그래도 올리려면 force: true를 넣어요. 재시도에서도 이 코드가 와요: 같은 내용과 파일의 다른 글이 같은 계정에서 대기, 발행 중, 발행됨, 예약 상태이고, 재시도하는 글보다 10분 넘게 먼저 만들어진 글이 아니면 실패한 시점과 상관없이 막아요. 이때 postId는 그 다른 글이고, targets는 보류된 이 글의 실패 대상이에요. 재시도된 것은 없어요. 그래도 보내려면 force: true를 넣어요.
unauthenticated401로그인 세션도 올바른 키도 없어요.
platform_rejected_content400, 404, 422, 503채널이 내용 자체를 거절했어요. 재시도해도 소용없어요. 읽기 요청은 채널의 상태 코드를 그대로 전하고, 발행은 503으로 알려요.
platform_auth_error503채널이 그 계정에 대한 포샌디의 접근을 거절했어요. 계정을 다시 연결해요. 포샌디 API 키 문제와 헷갈리지 않도록 503으로 보내요.
topic_tag_invalid422주제 태그에 마침표나 &가 있거나, UTF-8 기준 50바이트(한글 약 16자)를 넘어요. limit과 actual에 숫자가 와요. Threads가 받지 않는 형식이에요.
youtube_video_not_replaceable대상 기록이미 올라간 YouTube 영상을 고치면서 새 영상 파일을 보냈어요. YouTube는 올라간 영상의 파일을 바꾸지 않아요. 제목, 설명, 태그, 공개 범위만 고치거나 새 영상으로 올려요.
topic_tag_not_supported422고른 채널 중 하나가 주제 태그를 받지 않아요. 응답에 platform이 와요. 그 채널은 따로 올리거나 태그를 빼요.
api_key_revoked401폐기된 키예요. 새 키를 만들어요. 오타를 찾느라 시간 쓰지 않도록 invalid_api_key와 나눠 보내요.
api_key_expired401만료일이 지난 키예요. 새 키를 만들어요.
idempotency_key_reused422그 Idempotency-Key를 이미 다른 본문으로 썼어요. 새 요청에는 새 키를 써요.
publish_in_flight409글이 아직 나가는 중이에요. 이때 지우면 지운 뒤에 글이 올라갈 수 있어요. 몇 초 기다렸다가 다시 시도해요.
extension_outdated422사용자 브라우저의 포샌디 확장 프로그램이 이 작업을 하기에 옛 판이에요. 보낸 것은 없어요. 응답에 extensionVersion과 requiredVersion이 와요. 포샌디가 그 브라우저에 확장 업데이트를 요청하고, 네이버 작업이 없으면 확장이 스스로 바뀌어요. 1~2분 뒤 다시 불러요. 그래도 옛 판이면 대시보드 위쪽 확장 배지에서 「지금 업데이트」를 눌러 달라고 사용자에게 안내해요. 필요한 판이 아직 스토어 심사 중이면 메시지가 알려 주고 bridge.awaitingRelease가 true예요. 이때는 업데이트해도 찾는 판이 없고, 게시되면 브라우저가 알아서 설치해요. 그 전까지는 네이버에서 직접 해야 해요.
thread_and_content422content와 threadItems를 같이 보냈어요. 하나만 보내요.
thread_empty422threadItems가 빈 배열이에요.
container_expired503글이 나가기 전에 업로드 컨테이너가 만료됐어요. 글을 다시 만들어요.
permission_denied503채널이 권한 문제로 거절했어요.
invalid_json400요청 본문을 JSON으로 읽지 못했어요. 문법이 깨졌거나, 맨 위가 객체나 배열이 아니거나, 문자셋이 UTF-8이 아닌 경우예요.

코드별로 더 자세한 해결 방법은 안 될 때에 있어요.