# 오류 처리

> 오류 응답에서 확인할 필드와 코드별 원인을 알아보세요. 요청을 수정해야 할 때와 재시도할 수 있을 때를 구분해요.

Canonical: https://transcript1.com/ko/docs/errors
Language: ko
Last updated: 2026-11-05

오류는 요청 단계와 처리 단계로 나눠 확인하세요. 요청 오류가 나면 API가 호출을 받아들이지 않은 거예요. 요청은 접수됐지만 변환 도중 실패하면 작업의 `status`가 `failed`로 바뀌어요.

## 오류 응답 읽기

`/v1` API의 요청 오류는 아래 형식으로 반환해요.

```json
{
  "error": {
    "code": "insufficient_minutes",
    "message": "Not enough minutes",
    "retryable": false,
    "action_url": "https://transcript1.com/dashboard/billing",
    "minutes_needed": 20,
    "minutes_remaining": 3
  }
}
```

코드에서 오류를 분기할 때는 `code`를 사용하세요. `message`는 사용자에게 보여 줄 설명이며, 문구가 바뀔 수 있으니 분기 조건으로 쓰지 마세요. `retryable`이 `true`이면 잠시 후 같은 요청을 다시 시도할 수 있어요.

오류 종류에 따라 `retryable` 외에 다음 필드를 함께 반환해요.

| 필드 | 포함되는 오류 | 설명 |
|---|---|---|
| `action_url` | `insufficient_minutes`, `no_active_plan`, `topup_needs_user` | 요금제를 구매하거나 분량을 충전하는 페이지 주소 |
| `minutes_needed`, `minutes_remaining` | `insufficient_minutes` | 요청에 필요한 분량과 남은 분량. 필요한 분량을 아직 모르면 `null`이에요. |
| `reason`, `packs` | `topup_needs_user` | 카드로 결제하지 않은 이유와 요청한 팩 수 |
| `supported_platforms` | `unsupported_platform` | 지원 플랫폼 목록: `["youtube", "x", "tiktok", "instagram", "facebook", "bluesky", "twitch", "dailymotion", "pinterest", "linkedin", "snapchat"]` |
| `missing_chunks` | `upload_incomplete` | 아직 받지 못한 청크의 인덱스 목록 |
| `max_bytes` | `media_too_large` | 현재 허용되는 최대 파일 크기. 바이트 단위예요. |
| `params` | `invalid_request` | 본문·쿼리 검증에서 문제가 된 필드와 이유를 담은 객체 |

## 요청 단계에서 발생하는 오류

| HTTP 상태 | `code` | 그대로 재시도 | 원인과 대응 |
|---|---|---|---|
| 400, 422 | `invalid_request` | 불가 | 필수 파라미터나 본문 필드가 빠졌거나 값이 잘못됐어요. `diarize`와 `source: "captions"`, 또는 `reuse_recent`와 `webhook_url`을 함께 보낸 경우도 해당해요. 본문이 올바른 JSON인지도 확인하세요. |
| 422 | `invalid_url` | 불가 | YouTube, X, TikTok, Instagram, Facebook, Bluesky 또는 [지원 목록](https://transcript1.com/ko/docs/sources-and-options)에 있는 플랫폼의 영상 링크인지 확인하세요. 지정한 `platform`과 링크가 달라도 이 오류가 나요. |
| 422 | `unsupported_platform` | 불가 | 지정한 `platform`을 지원하지 않아요. |
| 422 | `unsupported_media` | 불가 | 파일 확장자가 지원 목록에 없어요. |
| 401 | `unauthorized` | 불가 | 인증 정보가 없거나 잘못됐거나 만료·폐기됐어요. 업로드 토큰이 잘못됐거나 만료된 경우도 해당해요. |
| 403 | `insufficient_scope` | 불가 | OAuth 토큰에 필요한 `transcriptions:read` 또는 `transcriptions:write` 권한이 없어요. |
| 403 | `email_not_verified` | 불가 | 변환 작업이나 실시간 세션을 시작하려면 먼저 이메일 인증을 마쳐야 해요. |
| 402 | `insufficient_minutes` | 불가 | 사용 가능한 분량이 0이거나 이번 변환에 필요한 분량보다 적어요. 실시간 세션에서는 시작한 1분보다 적을 때도 해당해요. [자동 충전](https://transcript1.com/ko/docs/billing-and-limits#auto-top-up)이 켜져 있으면 transcript1이 먼저 충전을 시도해요. `action_url`에서 분량을 구매하거나, 에이전트가 `POST /v1/topups` 또는 MCP 도구 `top_up`으로 구매하게 하세요. |
| 402 | `no_active_plan` | 불가 | 이용 중인 유료 요금제 없이 충전을 시도했어요. 변환 API가 아니라 `POST /v1/topups`와 대시보드 결제 엔드포인트에서 반환하는 오류예요. |
| 402 | `topup_needs_user` | `pending`, `in_progress`만 가능 | `POST /v1/topups`가 카드로 결제하지 않았어요. 이유는 `reason`에 있어요. `not_allowed`(인증 정보에 충전 구매 권한이 없거나 사용자가 팀 관리자가 아님), `cap_reached`(에이전트 월간 한도에 여유가 없음), `card_declined`, `authentication_required`, `no_card` 중 하나예요. 사용자는 `action_url`에서 팩을 구매할 수 있어요. `pending`(결제가 아직 확인되지 않음)과 `in_progress`(다른 충전이 진행 중)는 결제가 아직 완료될 수 있으니, 사용자를 구매 페이지로 보내지 말고 나중에 같은 `Idempotency-Key`로 재시도하세요. |
| 404 | `not_found` | 불가 | 해당 작업이 없거나 다른 팀에 속해 있어요. |
| 409 | `upload_incomplete` | 불가 | 누락된 청크를 보낸 뒤 업로드 완료 요청을 다시 보내세요. |
| 409 | `invalid_state` | 불가 | 업로드 대기 상태가 아니에요. 더 이상 청크를 전송하거나 업로드를 완료할 수 없어요. |
| 409 | `session_ended` | 불가 | 실시간 세션이 이미 끝나서 `POST /v1/live/sessions/:id/token`으로 토큰을 발급할 수 없어요. WebSocket `error` 이벤트로도 전달돼요. |
| 422 | `unsupported_language` | 불가 | 실시간 세션의 `language`가 지원되지 않아요. |
| 422 | `unsupported_translation` | 불가 | 실시간 세션의 `translate_to` 언어가 지원되지 않거나 실시간 번역을 사용할 수 없어요. |
| 409 | `idempotency_conflict` | 불가 | 이미 사용한 `Idempotency-Key`에 다른 파라미터를 보냈어요. |
| 413 | `media_too_large` | 불가 | 파일이 허용 크기를 넘었어요. [파일 업로드](https://transcript1.com/ko/docs/uploads)에서 한도를 확인하세요. |
| 413 | `invalid_request` | 불가 | JSON 본문이 1MB를 넘었어요. 파일 크기 초과는 `media_too_large`로 구분해요. |
| 429 | `rate_limited` | 가능 | 분당 요청 한도를 넘었어요. `Retry-After`에 지정된 초만큼 기다리세요. |
| 429 | `too_many_active_jobs` | 가능 | 진행 중인 변환 작업이나 완료 전 업로드가 한도를 넘었어요. `Retry-After`에 지정된 초만큼 기다리세요. |
| 429 | `too_many_live_sessions` | 가능 | 요금제에서 허용하는 동시 실시간 세션이 모두 사용 중이에요. `Retry-After`초(30)만큼 기다리세요. |
| 500 | `internal_error` | 가능 | transcript1 서버에서 오류가 났어요. 잠시 후 다시 요청하세요. |
| 503 | `service_unavailable` | 가능 | transcript1에 일시적인 장애가 있어요. 잠시 후 다시 요청하세요. |
| 503 | `live_capacity` | 가능 | 실시간 처리 용량이 모두 사용 중이에요. `Retry-After`초(30)만큼 기다린 뒤 세션을 다시 만드세요. |
| 503 | `live_unavailable` | 불가 | 지금은 실시간 변환을 사용할 수 없어요. |

브라우저에서 Bearer 토큰 대신 로그인 쿠키로 인증하면 요청 출처도 검사해요. 다른 사이트에서 보낸 요청에는 `403 forbidden_origin`을 반환해요. `Authorization` 헤더로 인증하는 API 호출에는 이 검사를 적용하지 않아요.

`429` 응답에는 항상 `Retry-After` 헤더가 있어요. 값은 다시 요청하기까지 기다릴 시간이며 초 단위예요. 실제 한도는 [요금제와 사용 한도](https://transcript1.com/ko/docs/billing-and-limits)에서 확인하세요.

## 안전하게 재시도하기

- 같은 요청을 그대로 다시 보낼 때는 `retryable`이 `true`인지 확인하세요. `rate_limited`, `too_many_active_jobs`, `too_many_live_sessions`, `live_capacity`, `internal_error`, `service_unavailable`, 그리고 `reason`이 `pending`이나 `in_progress`인 `topup_needs_user`가 해당해요. `Retry-After`가 있으면 지정한 시간만큼 기다리고, 없으면 재시도 간격을 늘리세요.
- `POST /v1/transcriptions`나 `POST /v1/topups`를 다시 보낼 때는 처음과 같은 `Idempotency-Key` 헤더를 사용하세요. 첫 요청이 이미 성공했더라도 중복 차감을 막을 수 있어요.
- 다른 오류는 원인을 해결하고 요청을 수정한 뒤 다시 보내세요.

## 실시간 변환

`POST /v1/live/sessions`와 다른 실시간 엔드포인트도 같은 오류 응답 형식을 써요. 위 표의 실시간 코드와 함께 `invalid_request`, `unsupported_language`, `insufficient_minutes`, `email_not_verified`도 해당해요. 세션 생성은 멱등하지 않기 때문에 `too_many_live_sessions`나 `live_capacity` 뒤에 다시 보내면 새 세션이 만들어져요.

세션이 만들어진 뒤의 문제는 HTTP 오류가 아니라 WebSocket의 `error` 이벤트와 종료 코드로 전달돼요. 모든 이벤트는 [실시간 변환](https://transcript1.com/ko/docs/live#websocket-protocol)을 참고하세요.

| 종료 코드 | 의미 |
|---|---|
| `1000` | 정상 종료예요. `session.ended` 뒤에 와요. |
| `1011` | 예기치 않은 서버 오류예요. 새 토큰으로 재개하세요. |
| `1012` | 서버 재시작이에요. `reconnect` 뒤에 와요. 새 토큰으로 재개하세요. |
| `4001` | 토큰이 유효하지 않거나, 만료됐거나, 이미 사용됐어요. |
| `4004` | 세션을 찾을 수 없거나 이미 끝났어요. |
| `4008` | 프로토콜 위반이에요. 홀수 길이이거나 너무 큰 오디오 프레임이에요. |
| `4010` | 같은 세션에 대한 더 새로운 연결이 이 연결을 대체했어요. |

`balance_exhausted`나 `idle_timeout` 같은 사유로 끝난 실시간 세션은 오류가 아니에요. 종료 사유와 함께 `session.ended`가 전달되고 세션은 저장돼요. [종료 사유](https://transcript1.com/ko/docs/live#end-reasons)를 참고하세요.

## 접수된 작업이 실패한 경우

작업의 `status`가 `failed`이면 객체에 `error: { "code", "message" }`가 들어 있어요. 생성 요청에 응답한 뒤에도 작업이 실패할 수 있어요. 상태를 조회할 때 `error`를 확인하거나 `transcription.failed` [웹훅](https://transcript1.com/ko/docs/webhooks)에서 원인을 읽으세요. 번역이 실패하면 `transcription.translation.failed` 웹훅이 전송되며, 이 때문에 전사가 실패하는 일은 없습니다.

| `error.code` | 원인 | 다시 시도하려면 |
|---|---|---|
| `media_unavailable` | 영상이 없거나 더 이상 접근할 수 없어요. | 그대로 재시도하지 말고 URL을 확인하세요. |
| `media_private` | 비공개 영상이에요. | 공개 게시물이나 공개·일부 공개 YouTube 영상으로 바꾸세요. |
| `login_required` | 영상을 보려면 플랫폼 로그인이 필요해요. | 자동 재시도하지 않는 영구 오류예요. |
| `no_audio` | 영상에 오디오 트랙이 없어요. | 자동 재시도하지 않는 영구 오류예요. |
| `age_restricted` | 연령 제한이 걸려 있어요. | 링크 대신 파일을 업로드하세요. |
| `live_stream` | 방송 중이거나 아직 시작하지 않은 라이브예요. | 방송이 끝난 뒤 다시 시도하세요. |
| `no_captions` | `source`가 `captions`인데 사용할 자막 트랙이 없어요. | `source: "auto"` 또는 `"stt"`로 바꾸세요. |
| `media_too_long` | 음성 텍스트 변환(STT)의 최대 길이인 5시간을 넘었어요. | 파일을 나눠서 보내세요. |
| `unsupported_media` | 파일을 읽지 못했거나 오디오 트랙이 없어요. | 지원 형식의 파일로 바꿔 보내세요. |
| `no_speech` | 음성이 감지되지 않았어요. | 그대로 재시도하지 마세요. |
| `unsupported_language` | 지원하지 않는 언어예요. | 그대로 재시도하지 말고 `language`를 지원하는 코드나 `"auto"`로 바꾸세요. |
| `insufficient_minutes` | 차감 시점에 사용 가능한 분량이 부족해졌고, 자동 충전이 켜져 있었더라도 분량을 추가하지 못했어요. | 분량을 충전한 뒤 다시 시도하세요. |
| `fetch_failed` | 미디어 다운로드를 여러 번 시도했지만 실패했어요. | 잠시 후 다시 시도할 수 있어요. |
| `transcription_failed` | 음성 텍스트 변환(STT)을 여러 번 시도했지만 실패했어요. | 잠시 후 다시 시도할 수 있어요. |

작업이 실패하거나 취소되면 비용이 들지 않아요. 이미 차감한 분량은 돌려드려요.
