에러 코드
에러 응답 형태 · HTTP 상태 코드 요약 · 전체 에러 코드 표.
요청의 성패는 HTTP 상태 코드로 알립니다. 2xx는 성공, 4xx는 보낸 정보로는 처리할 수 없었다는 뜻이고(대부분 error 코드가 함께 옵니다), 5xx는 우리 쪽 문제이거나 통신사·LiveKit 같은 외부 의존의 문제입니다.
에러 응답 형태
오류 본문은 평평한 봉투입니다. 코드는 항상 error 키에 있고, 진단에 필요한 값이 있으면 중첩 없이 같은 층에 함께 옵니다.
429 Too Many Requests
{ "error": "number_busy", "current": 1, "limit": 1 }같은 층에 올 수 있는 키는 다음이 전부입니다. 어떤 코드에 무엇이 붙는지는 아래 에러 코드 표의 설명에 적혀 있습니다.
| 키 | 타입 | 언제 |
|---|---|---|
error | string | 항상. 아래 표의 코드 중 하나입니다. |
field | string | 필수 값이 빠졌을 때 그 필드 이름. |
supported | string[] | 값이 허용 목록 밖일 때 그 목록. |
current · limit | number | 동시통화 한도에 걸렸을 때 현재값과 한도. |
end_reason | string | 실제로 다이얼을 시도한 뒤 실패했을 때. 값의 뜻은 통화 객체의 종료 사유와 같습니다. |
reason | string | 녹음 스트림·링크를 못 준 이유 (pending · none · failed). |
status | string | 종료·옵션 변경을 거절했을 때 그 통화의 현재 상태. |
candidates | object[] | 발신 번호를 골라야 할 때 후보 목록 (number_id · phone · workspace_name). |
/api/my/*의 조회 실패 404만 FastAPI 기본 봉투({"detail": "call not found"})를 씁니다. 같은 라우터라도 녹음 링크 거절은 error 봉투이고, /api/v1/*는 전부 error입니다. 중첩된 error.code 구조는 어디에도 없습니다.
HTTP 상태 코드
| 코드 | 뜻 |
|---|---|
| 200 | 요청이 처리됐습니다. |
| 201 | 발신을 접수했습니다 — 통화는 비동기로 진행됩니다. |
| 202 | 종료를 요청했습니다. 실제 종료는 작별 인사가 끝난 뒤입니다. |
| 206 | 요청한 녹음 파일의 일부 바이트를 반환했습니다. |
| 400 | 본문이나 값이 잘못됐습니다. 그대로 재시도하면 같은 결과입니다. |
| 401 | 자격증명이 없거나 무효합니다. |
| 403 | 자격증명은 유효하지만 그 자원에 권한이 없습니다. |
| 404 | 없거나, 내 것이 아닙니다 — 둘을 구분해 알려주지 않습니다. |
| 409 | 지금 상태에서는 할 수 없는 요청입니다. 재시도해도 대개 같습니다. |
| 416 | 요청한 녹음 바이트 범위가 파일 범위를 벗어났습니다. |
| 422 | 값은 형식에 맞지만 설정·문맥과 맞지 않습니다. |
| 429 | 한도를 넘었습니다. 잠시 뒤 재시도하세요. |
| 502 | 외부 의존(LiveKit 등)이 일시적으로 실패했습니다 — 재시도할 만합니다. |
| 503 | 그 기능이 이 배포에 설정돼 있지 않거나 회선을 잡지 못했습니다. |
발신 거절은 대부분 전화를 걸기 전에 납니다 — 그 경우 통화 기록도 요금도 생기지 않습니다. 실제로 다이얼한 뒤의 실패에는 end_reason이 함께 옵니다.
에러 코드
REST와 MCP가 같은 코드 집합을 씁니다. 알파벳순입니다.
| error | HTTP | 뜻 |
|---|---|---|
call_not_active | 409 | 끊거나 옵션을 바꾸려는 통화가 진행 중이 아닙니다. 이미 끝났거나 아직 연결 전입니다 — 현재 status가 함께 옵니다. |
call_not_found | 404 | 그 통화가 없거나 내 번호의 통화가 아닙니다. 둘을 구분해 알려주지 않습니다(존재 은닉). |
concurrency_limit_platform_exceeded | 429 | 플랫폼 전체 동시통화 한도를 넘었습니다. 잠시 뒤 재시도하세요. |
concurrency_limit_workspace_exceeded | 429 | 워크스페이스 동시통화 한도를 넘었습니다. |
dispatch_failed | 503 | 다이얼했지만 그 밖의 이유로 회선을 잡지 못했습니다. 실제로 시도한 결과라 end_reason이 함께 옵니다. |
from_number_required | 422 | 발신 번호를 지정하지 않았는데 접근 가능한 번호가 둘 이상입니다. candidates에서 하나를 골라 다시 부르세요. OAuth 연결에서만 납니다. |
hangup_failed | 502 | 종료 신호 전송이 일시적으로 실패했습니다 — 재시도할 만합니다. |
hangup_unavailable | 503 | 이 배포에 종료 기능이 설정돼 있지 않습니다. |
invalid_api_key | 401 | Authorization 헤더가 없거나 Bearer가 아니거나 모르는 키입니다. 키는 재발급하면 이전 키가 즉시 무효가 됩니다. |
invalid_language | 400 | language가 ko · vi · en · zh · es 중 하나가 아닙니다. 지원 목록이 supported로 함께 옵니다. |
invalid_max_duration_sec | 400 | max_duration_sec가 gateway의 CallPolicy 검증을 통과하지 못했습니다. |
invalid_phone_format | 400 | 착신번호가 국내형 010/070(11자리)도, 짧은 내선도 아닙니다. 국제번호는 지원하지 않습니다. |
invalid_request_body | 400 | 본문이 JSON이 아니거나 형식이 맞지 않습니다. 통화 옵션 변경에서는 아는 옵션이 하나도 없거나, 값의 타입이 다르거나, 허용 밖 값일 때(예: 지원하지 않는 language) 나며, 지원 목록이 supported로 함께 옵니다. |
language_unavailable | 422 | 통화 중 언어 전환 요청인데, 이 통화가 쓰는 TTS 프로바이더가 그 언어를 말할 수 없습니다. 통화 중에는 프로바이더를 바꿀 수 없어 전환만 거절되고 통화는 그대로 진행됩니다. 현재 프로바이더가 provider로 함께 옵니다. |
missing_required_field | 400 | 필수 값이 비어 있습니다. 어느 필드인지가 field로 함께 옵니다(현재는 to). |
max_duration_exceeds_policy | 400 | max_duration_sec가 이 번호에 적용되는 최대 통화 시간보다 깁니다. 현재 허용 상한이 maximum으로 함께 옵니다. |
no_agent_endpoint | 422 | 자체 Agent를 바인딩했는데 chat URL이 비어 있습니다. 전화를 걸지 않고 거절합니다. |
no_number_available | 422 | 발신 가능한(배정된) 번호가 하나도 없습니다. OAuth 연결에서만 납니다. |
not_configured | 503 | 이 배포에 녹음 재생 링크 기능이 켜져 있지 않습니다. |
number_busy | 429 | 이 번호의 동시통화 한도를 넘었거나(current·limit 동반), 다이얼했더니 상대가 통화 중이었습니다(SIP 486 — end_reason 동반). 두 경우를 함께 오는 키로 가릅니다. |
number_not_allowed | 403 | 지정한 번호에 접근 권한이 없습니다. OAuth 연결에서만 납니다. |
number_not_found | 404 | 다이얼했더니 없는 번호였습니다 (SIP 404). end_reason이 함께 옵니다. |
number_required | 422 | from_number_required와 같은 상황을 MCP end_call 도구가 부르는 이름입니다 — 그 도구의 인자 이름이 number이기 때문입니다. |
options_failed | 502 | 통화 옵션 변경 신호를 보내지 못했습니다 — 일시적이라 재시도할 수 있습니다. |
options_unavailable | 503 | 통화 옵션 기능이 이 배포에 설정돼 있지 않습니다. |
range_not_satisfiable | 416 | 요청한 녹음 바이트 범위가 파일 범위를 벗어났습니다. Range 값을 다시 계산하세요. |
recording_not_available | 409 | 녹음이 available이 아닙니다. 어느 상태인지가 reason으로 함께 옵니다 — pending이면 마감 중이라 몇 초 뒤 되고, none이면 녹음하지 않은 통화, failed면 재시도해도 같습니다. v1 직접 스트림과 녹음 링크가 함께 씁니다. |
recording_storage_error | 502 | 완료된 녹음 파일을 저장소에서 읽지 못했습니다. 일시적일 수 있으므로 재시도하세요. |
recording_unavailable | 503 | 이 배포에 v1 녹음 직접 스트리밍 기능이 설정돼 있지 않습니다. |
room_unknown | 409 | 종료·옵션 변경 신호를 보낼 통화 세션을 찾지 못했습니다. 수신 통화에서 gateway가 재시작된 뒤 납니다. |
unauthorized | 401 | MCP 엔드포인트에 자격증명 없이 접근했거나 OAuth 토큰이 무효합니다. |
workspace_inactive | 403 | 번호가 워크스페이스에 배정돼 있지 않거나 워크스페이스가 비활성 상태입니다. |
- 종료·옵션 변경 거절은 통화를 죽이지 않습니다.
call_not_active·room_unknown·hangup_failed·options_failed어느 쪽이든 통화는 살아 있으니, 사유를 받고 대화를 이어가세요. 202는 접수이지 적용이 아닙니다 — 통화 옵션은 상태 조회의options_pending으로 반영을 확인하세요.- 통화가 어떻게 끝났는지는 오류가 아니라 통화 객체의 값입니다 — Calls API › 통화 객체의
end_reason를 보세요.
엔드포인트별로 어떤 코드가 나는지는 각 엔드포인트 절에 적지 않습니다 — 코드가 늘 때마다 두 곳을 고쳐야 하고, 실제로 한쪽이 부분집합인 채로 남은 적이 있습니다.