# 오류

> Nodaro API의 모든 오류는 하나의 응답 형식에 HTTP 상태와 고정된 코드를 담습니다. 각 코드의 의미, 재시도할 오류, 실패한 작업이 원인을 알리는 방식을 확인하세요.

Source: https://nodaro.ai/ko/docs/developers/api/errors

모든 **Nodaro API 오류**는 HTTP 상태와, 고정된 `code`가 담긴 하나의 JSON 응답 형식을 반환합니다. 따라서 클라이언트는 메시지를 파싱하지 않고 코드로 분기할 수 있습니다. 실패는 두 종류입니다. 호출 자체가 거부되어 아무것도 시작되지 않는 **요청 오류**와, 시작된 생성이 나중에 실패해 작업에 이유를 남기는 **작업 실패**입니다.

## 오류 응답 형식
```json
{ "error": { "code": "rate_limited", "message": "Too many requests. Max 30 per minute." } }
```

- `code`는 바뀌지 않는 슬러그입니다. 이 값으로 분기하세요.
- `message`는 사람이 읽는 텍스트입니다. 바뀔 수 있으므로 이 값과 비교하지 마세요.
- 일부 오류는 필드를 추가하며, 아래 각 코드에 함께 적어 두었습니다. 대부분 `error` 안에 들어가지만, `already_running`은 `executionId`를 `error` 옆에 두고, 실행 전 허용량 확인에 실패하면 `required`와 `remaining`이 최상위에 들어갑니다.

OAuth 토큰 엔드포인트만 예외입니다. 이 엔드포인트는 OAuth 형식으로 응답합니다. 예: `{ "error": "invalid_grant", "error_description": "…" }`. [OAuth 앱](https://nodaro.ai/docs/developers/oauth)을 참조하세요.

## 재시도할 오류
| 상태 | 해야 할 일 |
| --- | --- |
| `5xx` | 대개 일시적입니다. 지수 백오프로 재시도하세요. |
| `429` | 기다린 뒤 백오프로 재시도하세요. 예: 5초, 10초, 20초. `Retry-After` 헤더가 있으면 그 값을 따르세요. |
| `402` | 크레딧을 추가하거나 워크스페이스 관리자에게 예산을 요청한 뒤 재시도하세요. |
| 그 밖의 `4xx` | 먼저 요청을 고치세요. 같은 요청은 다시 실패합니다. |

서버 오류 세 가지는 주의가 필요합니다.

- `503 price_not_configured`는 재시도해도 성공하지 않습니다. 운영자가 가격을 설정할 때까지 이 배포 환경에서 해당 모델에는 가격이 없습니다.
- 비동기 구조화 LLM 라우트의 `503 provider_unavailable`은, 언어 모델 호출을 nodaro.ai로 보내는 설치에서는 영구적인 오류입니다.
- 소셜 네트워크에 게시할 때 `503 publish_retryable`은 아무것도 게시되지 않아 같은 요청을 다시 보내도 안전하다는 뜻입니다. 하지만 `500 publish_failed`는 결과를 알 수 없다는 뜻이며, 다시 보내면 두 번 게시될 수 있습니다.

## 오류 코드
### 400 잘못된 요청
| 코드 | 의미 |
| --- | --- |
| `validation_error` | 본문 형식이 잘못되었거나, UUID가 잘못되었거나, 필드가 유효하지 않거나, 커서 형식이 잘못되었습니다. |
| `limit_reached` | 활성 여부와 관계없이 API 토큰이 이미 10개 있습니다. 먼저 하나를 삭제하세요. |
| `invalid_workflow` | 토큰의 `workflowIds` 항목이 내 개인 공간의 워크플로가 아닙니다. |
| `token_workspace_mismatch` | 한 워크스페이스에 묶인 토큰을, 다른 워크스페이스를 지정한 `X-Nodaro-Workspace` 헤더와 함께 보냈습니다. |
| `locked_field` | 실행에서 외부로 보내거나 가져오는 노드의 대상 위치를 바꾸려고 했습니다. [실행에서 바꿀 수 없는 것](https://nodaro.ai/docs/developers/api/workflows#what-a-run-cannot-change)을 참조하세요. |
| `image_required` | 텍스트로 동영상을 만들 수 없는 모델에서 시작 프레임 없이 `POST /v1/generate-video`를 보냈습니다. 레퍼런스로 대신할 수 있는지는 메시지에 나옵니다. |
| `sequence_execution_required` | 실행에 스튜디오 프로덕션 API로 생성해야 하는 스튜디오 프로덕션 노드가 포함되어 있습니다. |
| `not_workspace_scoped` | 워크스페이스에 속하지 않은 워크플로의 `visibility`를 변경했습니다. |
| `advanced_mode_unsupported` | 직접 경로가 없는 모델에 `advancedMode: true`를 보냈습니다. |
| `continuation_subset_required` | `nodeIds` 없이 `continueFromExecutionId`를 보냈습니다. [실행 이어가기](https://nodaro.ai/docs/developers/api/workflows#continue-a-run)를 참조하세요. |
| `continuation_workflow_mismatch` | `continueFromExecutionId`로 지정한 실행이 다른 워크플로를 실행했습니다. |
| `continuation_version_mismatch` | `continueFromExecutionId`로 지정한 실행이 워크플로의 게시된 앱 버전을 실행했습니다. |
| `render_final_node_not_found` | `renderFinal.renderNodeId`가 워크플로의 어떤 노드도 가리키지 않습니다. |
| `render_final_not_a_render` | `renderFinal.renderNodeId`가 EDL 적용 렌더가 아닌 노드를 가리킵니다. |

### 401 인증 실패
| 코드 | 의미 |
| --- | --- |
| `unauthorized` | 토큰이 없거나, 잘못되었거나, 만료되었거나, 취소되었습니다. |

### 402 결제 필요
이 코드는 Nodaro Cloud와 과금하는 배포 환경에서 반환됩니다.

| 코드 | 추가 필드 | 의미 |
| --- | --- | --- |
| `insufficient_credits` | `required`, `balance` | 계정의 크레딧이 부족합니다. 결제 계정이 하나인 배포 환경에서는 배포 환경의 풀이 비었다는 뜻입니다. 이때 오류에는 `required`만 있고 `balance`는 없으며, 결제 계정만 크레딧을 추가할 수 있습니다. |
| `insufficient_app_credits` | | 게시된 앱의 실행에서 계정의 크레딧이 부족합니다. |
| `budget_exceeded` | | 워크스페이스가 비용을 내는 사용에서, 워크스페이스 예산으로 실행 비용을 충당할 수 없습니다. 워크스페이스 관리자에게 추가 예산을 요청하세요. |
| `member_cap_exceeded` | | 워크스페이스가 비용을 내는 사용에서, 그 워크스페이스의 내 사용 한도에 도달했습니다. |
| `user_allowance_exceeded` | `required`, `remaining` | 사용자별 허용량을 적용하는, 결제 계정이 하나인 배포 환경에서 내 허용량으로 실행 비용을 충당할 수 없습니다. 결제 계정만 허용량을 늘릴 수 있습니다. |
| `instance_cap_reached` | | 연결된 셀프 호스팅 설치의 OAuth 토큰입니다. 설치가 자신을 연결한 계정에서 월간 한도를 모두 사용했습니다. **연결된 인스턴스**에서 한도를 늘리거나 없애세요. |

### 403 접근 거부
| 코드 | 추가 필드 | 의미 |
| --- | --- | --- |
| `forbidden` | | 토큰에 워크플로 범위가 있고 요청이 그 범위를 벗어났거나, 라우트에 로그인한 세션이 필요합니다. [토큰을 일부 워크플로로 제한](https://nodaro.ai/docs/developers/api/authentication#limit-a-token-to-some-workflows)과 [로그인한 세션이 필요한 라우트](https://nodaro.ai/docs/developers/api/authentication#routes-that-need-a-signed-in-session)를 참조하세요. |
| `access_blocked` | | Nodaro Cloud와 Business 에디션에서 관리자가 계정을 차단했습니다. 세션, API 토큰, OAuth 토큰 등 계정의 모든 자격 증명이 거부됩니다. 계정의 일정과 트리거가 멈추고, 게시된 앱은 `404 not_found`로 응답합니다. 잘못된 차단이라고 생각되면 지원팀에 문의하세요. |
| `insufficient_scope` | `missingScope` | OAuth 토큰에 라우트에 필요한 스코프가 없습니다. 더 넓은 스코프로 사용자에게 다시 동의를 받으세요. |
| `in_app_only` | | 이 라우트는 Workflow Copilot처럼 Nodaro 웹 앱 전용입니다. |
| `node_not_available` | | 이 배포 환경이 내 계정에 해당 노드나, 노드에서 고른 옵션을 제공하지 않습니다. 메시지에 그 이름이 나옵니다. 크레딧은 차감되지 않습니다. |
| `model_not_available` | | 보낸 `provider`가 이 배포 환경에서 제공하지 않는 모델입니다. 다른 모델을 고르세요. 크레딧은 차감되지 않습니다. |
| `edition_required` | `required_edition` | 이 라우트에는 더 높은 에디션이 필요합니다. 파이프라인 분기에는 `cloud`, API 토큰 관리에는 `business`가 필요합니다. |
| `not_a_member` | | `X-Nodaro-Workspace` 헤더가 내가 활성 멤버가 아닌 워크스페이스를 가리킵니다. |
| `member_suspended` | | 워크스페이스의 내 멤버십이 정지되었습니다. |
| `personal_space_disabled` | | 조직이 워크스페이스 안에서만 새 항목을 만들 수 있도록 제한합니다. 워크스페이스 헤더를 보내세요. |
| `project_create_not_allowed` | | 워크스페이스 관리자만 프로젝트를 만들 수 있습니다. |
| `workspace_archived` | | 워크플로 공유처럼, 보관된 워크스페이스에 쓰기를 시도했습니다. 그곳에 새로 만들면 대신 `409`를 반환합니다. |
| `not_permitted` | | 이 워크플로는 내가 옮길 수 없거나, 그곳으로는 옮길 수 없습니다. |
| `sso_required` | | 배포 환경이 자체 ID 공급자를 통한 로그인만 허용하는데, 이 세션의 계정은 그 공급자로 만들어지지 않았습니다. 토큰에는 영향이 없습니다. |
| `subscription_required` | | 종량제 계정이 웹 에디터에서 크레딧을 썼습니다. 종량제 크레딧은 API, SDK, CLI, MCP에서 사용할 수 있으므로, 토큰 호출에서는 이 코드가 반환되지 않습니다. |
| `api_tokens_payer_only` | | 결제 계정이 하나인 배포 환경에서는 그 계정만 API 토큰을 만들 수 있습니다. |
| `payer_balance_jwt_only` | | 그런 배포 환경에서 결제 계정이 토큰으로 풀 잔액을 읽었습니다. 잔액은 결제 계정의 브라우저 세션에서만 읽을 수 있습니다. |
| `payer_required` | | 결제 계정의 브라우저 세션이 아닌 곳에서 배포 환경의 결제 라우트를 호출했습니다. |

### 404 찾을 수 없음
| 코드 | 의미 |
| --- | --- |
| `not_found` | 리소스가 없거나, 내가 볼 수 없습니다. 두 경우는 의도적으로 같은 응답을 반환하므로, id로 무언가가 존재하는지 알아낼 수 없습니다. |
| `workspace_not_found` | 워크스페이스가 비용을 내는 사용에서, 지정한 워크스페이스가 존재하지 않습니다. |
| `continuation_not_found` | `continueFromExecutionId`로 지정한 실행이 없거나 내 것이 아닙니다. |

### 409 충돌
| 코드 | 추가 필드 | 의미 |
| --- | --- | --- |
| `already_running` | `executionId` | 워크플로에 이미 진행 중인 실행이 있습니다. 대신 그 실행을 폴링하세요. |
| `continuation_not_completed` | | `continueFromExecutionId`로 지정한 실행이 아직 진행 중이거나, 실패했거나, 취소되었습니다. 완료된 실행만 이어갈 수 있습니다. |
| `workflow_conflict` | `currentVersion`, `currentUpdatedAt`, `currentRecord` | 보낸 버전 이후 워크플로가 변경되었습니다. `currentRecord`에 변경 사항을 병합한 뒤 다시 저장하세요. |
| `workspace_archived` | | 보관된 워크스페이스에 항목을 만들거나 옮기려고 했습니다. |
| `workspace_has_no_default_project` | | 워크스페이스에서 만들 때 프로젝트를 지정하지 않았는데, 워크스페이스에 프로젝트가 없습니다. 프로젝트를 지정하세요. |
| `move_blocked` | | 과제용으로 만든 항목이라 옮길 수 없습니다. |
| `production_capability_required` | | 일반 워크플로 저장이, 전용 API가 필요한 스튜디오 프로덕션을 변경했습니다. |
| `retained_image_in_use` | | 삭제하면 보호된 이미지가 제거되거나, 프로덕션에 아직 그 이미지를 쓰는 작업이 있습니다. 먼저 그 작업을 완료하거나 취소하세요. |

### 413, 422, 429
| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 413 | | 계정이 저장 공간 한도를 넘었습니다. SDK는 `limitBytes`와 함께 `StorageExceededError`를 발생시킵니다. |
| 422 | `job_blocked` | 배포 환경의 작업 정책이 실행 전에 생성을 거부했습니다. 작업은 만들어지지 않았고 크레딧도 차감되지 않았습니다. `message`를 그대로 사용자에게 보여 주고, 같은 요청을 재시도하지 마세요. |
| 422 | `upload_blocked` | 배포 환경의 업로드 정책이 파일을 저장하기 전에 거부했습니다. `message`를 그대로 보여 주세요. |
| 422 | `offsets_not_applied` | `POST /v1/edit-plan`이 원시 `offsets` 필드를 받았습니다. 먼저 각 오프셋을 해당 소스의 `sources[].offsetMs`에 쓰세요. [**편집 계획**](https://nodaro.ai/docs/nodes/video/edit-plan#api)을 참조하세요. |
| 422 | `master_offset` | `POST /v1/edit-plan`이 마스터나 트랜스크립트를 만든 소스에서 0이 아닌 오프셋을 받았습니다. 크레딧은 차감되지 않습니다. |
| 429 | `rate_limited` | 개인 API 토큰의 토큰별 한도입니다. |
| 429 | `rate_limit_exceeded` | 그 밖의 모든 한도입니다. 주소별, 라우트별 한도나 게시된 앱의 일일 실행 한도가 해당합니다. |
| 429 | `too_many_downloads` | 이미 동영상 가져오기 4개가 실행 중입니다. |

`job_blocked`와 `upload_blocked`는 이런 정책을 등록한 배포 환경에서만 발생합니다. `429` 코드는 [요청 한도](https://nodaro.ai/docs/developers/api/rate-limits)를 참조하세요.

### 500, 503
| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 500 | `internal_error` | 서버 오류이거나, 서버가 의존하는 서비스의 장애입니다. 백오프로 재시도하세요. |
| 503 | `price_not_configured` | 요청한 모델에 이 배포 환경의 가격이 없으므로, 서버가 잘못 청구하지 않도록 요청을 거부합니다. 재시도해도 소용없습니다. |
| 503 | `rate_limit_unavailable` | 크레딧을 쓰는 라우트가 요청 한도를 확인하지 못했습니다. 나중에 재시도하세요. |
| 503 | `provider_unavailable` | 모델의 공급자를 사용할 수 없습니다. |
| 503 | `billing_unavailable` | 이 인스턴스에서는 아직 사용량 보고를 사용할 수 없습니다. |

## 작업이 나중에 실패한 경우
성공한 요청에서도 작업이 실패할 수 있습니다. 이때 작업에는 `error_message`가 있고, 두 종류의 실패에는 구조화된 `error_hint`도 있습니다.

- `{ "kind": "safety-block", "class": "copyright" | "likeness" | "safety", "retried": boolean, "suggestedProvider"?: string }`: 모델의 안전 필터가 요청을 차단한 경우입니다.
- `{ "kind": "policy-block", "policyId": string, "reason": string, "hookPoint": "request" | "result" }`: 배포 환경의 정책이 요청을 거부한 경우입니다.

두 경우 모두 항상 환불되며, 작업의 `credit_status`에서 확인할 수 있습니다. 각 필드의 의미와 다른 모델을 시도할 시점은 [작업이 실패한 이유](https://nodaro.ai/docs/developers/api/jobs#why-a-job-failed)를 읽어 보세요.

## SDK의 오류
`@nodaro/sdk`는 실패한 응답마다 타입이 지정된 오류를 던집니다. 가장 구체적인 클래스부터 잡으세요.

| 클래스 | 상태 | 추가 필드 |
| --- | --- | --- |
| `UnauthorizedError` | 401 | |
| `ForbiddenError` | 403 | 코드가 `insufficient_scope`이면 `missingScope` |
| `NotFoundError` | 404 | |
| `InsufficientCreditsError` | 402 | `required`, `available` |
| `StorageExceededError` | 413 | `limitBytes` |
| `WorkflowConflictError` | 409 | `currentVersion`, `currentUpdatedAt`, `currentRecord`. 스튜디오의 `production_busy` 충돌에도 쓰입니다. |
| `RateLimitedError` | 429 | |
| `JobBlockedError` | 422 | |
| `StudioOpError` | 4xx | `opIndex`: 스튜디오 일괄 처리에서 거부된 연산의 위치 |
| `NodaroError` | 모두 | 기본 클래스입니다. `code`, `status`, `message`가 있습니다. |

```ts

NodaroError,
ForbiddenError,
InsufficientCreditsError,
RateLimitedError,
} from '@nodaro/sdk'

try {
await client.workflows.run(workflowId)
} catch (err) {
if (err instanceof InsufficientCreditsError) {
showPaywall({ required: err.required, available: err.available })
} else if (err instanceof ForbiddenError && err.missingScope) {
requestConsent([err.missingScope])
} else if (err instanceof RateLimitedError) {
await new Promise((r) => setTimeout(r, 5_000))
} else if (err instanceof NodaroError) {
console.error(`API error ${err.status} (${err.code}): ${err.message}`)
} else {
throw err // a network failure, not an API error
}
}
```

폴링 도우미는 `JobFailedError`, `JobTimeoutError` 같은 자체 오류를 추가합니다. [단일 노드 실행](https://nodaro.ai/docs/developers/api/nodes#example-generate-an-image)을 참조하세요.

## CLI의 오류
| 종료 코드 | 의미 |
| --- | --- |
| `0` | 성공입니다. |
| `1` | 인증 실패, 찾을 수 없음, 잘못된 인수, 네트워크 오류입니다. |
| `2` | `--watch`가 끝났고 실행이 실패했습니다. |
| `3` | 작업이 검토를 위해 보류되어 `--watch`가 멈췄습니다. 실패가 아니므로 나중에 다시 확인하세요. |
| `130` | `--watch`가 끝났고 실행이 취소되었습니다. |

`--json`을 쓰면 CLI는 코드 2, 3, 130을 쓰지 않고 페이로드를 출력한 뒤 정상 종료합니다. 따라서 `.status`를 직접 확인하세요.

## 자주 묻는 질문

### Nodaro API 오류는 어떤 형태인가요?

모든 오류에는 HTTP 상태와 { "error": { "code": "…", "message": "…" } } 본문이 있습니다. code는 바뀌지 않으므로 code로 분기하세요. message는 사람이 읽는 문구이며 바뀔 수 있습니다.

### 어떤 Nodaro API 오류를 재시도해야 하나요?

5xx 오류와 429는 지수 백오프로 재시도하세요. 예를 들어 5초, 10초, 20초 뒤에 다시 보냅니다. 4xx 오류는 요청을 바꾸지 않으면 같은 요청이 다시 실패하므로 그대로 재시도하지 마세요. 서버 오류 중에서는 503 price_not_configured가 예외입니다.

### 402 insufficient_credits는 무슨 뜻인가요?

계정에 실행을 시작할 크레딧이 부족하다는 뜻입니다. 오류에는 실행에 필요한 크레딧인 required와, 대개 balance가 함께 들어 있습니다. Nodaro Cloud에서 크레딧을 추가한 뒤 요청을 다시 보내세요.

### 실패한 작업의 error_hint는 무엇인가요?

모델의 안전 필터와 배포 환경의 정책, 두 종류의 실패에 대해 실패한 작업에 붙는 구조화된 이유입니다. error_message를 파싱하지 말고 이 값을 읽으세요. 예를 들어 제안된 모델을 사용자에게 보여 줄 때 씁니다.

### 라우트가 403 in_app_only를 반환하는 이유는 무엇인가요?

그 라우트가 Nodaro 웹 앱 자체의 세션 전용이기 때문입니다. 예를 들어 Workflow Copilot이나 저장된 Webhook 출력 자격 증명이 그렇습니다. API 토큰과 OAuth 토큰으로는 호출할 수 없습니다. 대신 워크플로 엔드포인트, SDK, MCP를 사용하세요.
