# Errors

> Every Nodaro API error has an HTTP status and a stable code in one envelope. Learn what each code means, which errors to retry, and how failed jobs report why.

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

Every **Nodaro API error** returns an HTTP status and one JSON envelope with a stable `code`, so your client can dispatch on the code instead of parsing messages. There are two kinds of failure: a **request error**, when the call itself is refused and nothing starts, and a **job failure**, when a generation that started fails later and reports why on the job.

## The error envelope

```json
{ "error": { "code": "rate_limited", "message": "Too many requests. Max 30 per minute." } }
```

- `code` is a stable slug. Dispatch on it.
- `message` is text for people. It can change, so never match on it.
- Some errors add fields, listed with each code below. Most sit inside `error`; `already_running` puts `executionId` beside `error`, and a failed pre-run allowance check puts `required` and `remaining` at the top level.

The OAuth token endpoint is the one exception: it answers in the OAuth format, for example `{ "error": "invalid_grant", "error_description": "…" }`. See [OAuth apps](https://nodaro.ai/docs/developers/oauth).

## Which errors to retry

| Status | What to do |
| --- | --- |
| `5xx` | Usually temporary. Retry with exponential backoff. |
| `429` | Wait, then retry with backoff: for example 5, then 10, then 20 seconds. Honor a `Retry-After` header when there is one. |
| `402` | Add credits, or ask a workspace admin for budget, then retry. |
| Other `4xx` | Fix the request first. The same request fails again. |

Three server errors need care:

- `503 price_not_configured` does not succeed on retry: the model has no price on this deployment until its operator sets one.
- `503 provider_unavailable` on the asynchronous structured LLM route is permanent on an install that sends its language-model calls to nodaro.ai.
- When publishing to social networks, `503 publish_retryable` means nothing was posted and the same request is safe to send again, but `500 publish_failed` means the outcome is unknown, and sending again can post twice.

## Error codes

### 400 Bad Request

| Code | Meaning |
| --- | --- |
| `validation_error` | A malformed body, a bad UUID, an invalid field or a malformed cursor. |
| `limit_reached` | You already have 10 API tokens, active or not. Delete one first. |
| `invalid_workflow` | A token's `workflowIds` entry is not a workflow in your personal space. |
| `token_workspace_mismatch` | A token bound to one workspace was sent with an `X-Nodaro-Workspace` header naming another. |
| `locked_field` | A run tried to change where an outbound node sends or fetches. See [What a run cannot change](https://nodaro.ai/docs/developers/api/workflows#what-a-run-cannot-change). |
| `image_required` | `POST /v1/generate-video` without a start frame, on a model that cannot make video from text. The message says whether references would work instead. |
| `sequence_execution_required` | The run includes Studio production nodes that must be generated through the Studio production API. |
| `not_workspace_scoped` | You changed `visibility` on a workflow that is not in a workspace. |
| `advanced_mode_unsupported` | `advancedMode: true` on a model that has no direct lane. |

### 401 Unauthorized

| Code | Meaning |
| --- | --- |
| `unauthorized` | The token is missing, invalid, expired or revoked. |

### 402 Payment Required

These codes come from Nodaro Cloud and from deployments that bill.

| Code | Extra fields | Meaning |
| --- | --- | --- |
| `insufficient_credits` | `required`, `balance` | The account is out of credits. On a deployment with one billing account, it means the deployment's pool is empty: the error then carries `required` but never `balance`, and only the billing account can add credits. |
| `insufficient_app_credits` | | A published app's run: the account is out of credits. |
| `budget_exceeded` | | Work paid by a workspace: the workspace budget cannot cover the run. Ask a workspace admin for more. |
| `member_cap_exceeded` | | Work paid by a workspace: your own spending cap in that workspace is reached. |
| `user_allowance_exceeded` | `required`, `remaining` | A deployment with one billing account that enforces per-user allowances: your allowance cannot cover the run. Only the billing account can raise it. |
| `instance_cap_reached` | | An OAuth token of a connected self-hosted install: the install has spent its monthly cap on the account that connected it. Raise or remove the cap under **Connected Instances**. |

### 403 Forbidden

| Code | Extra fields | Meaning |
| --- | --- | --- |
| `forbidden` | | The token's workflow scope does not include this workflow, or the route needs a signed-in session. See [Authentication](https://nodaro.ai/docs/developers/api/authentication#routes-that-need-a-signed-in-session). |
| `insufficient_scope` | `missingScope` | An OAuth token lacks a scope the route needs. Send the user through consent again with the wider scope. |
| `in_app_only` | | The route exists only for the Nodaro web app, such as the Workflow Copilot. |
| `edition_required` | `required_edition` | The route needs a higher edition: `cloud` for pipeline branching, `business` for API token management. |
| `not_a_member` | | The `X-Nodaro-Workspace` header names a workspace you are not an active member of. |
| `member_suspended` | | Your membership in the workspace is suspended. |
| `personal_space_disabled` | | Your organization lets you create work only inside a workspace. Send the workspace header. |
| `project_create_not_allowed` | | Only the workspace's admins may create projects. |
| `workspace_archived` | | A write into an archived workspace, such as sharing a workflow. Creating work there answers `409` instead. |
| `not_permitted` | | The workflow is not yours to move, or not yours to move there. |
| `sso_required` | | The deployment restricts sign-in to its identity provider, and this session's account was not created through it. Tokens are not affected. |
| `subscription_required` | | A pay-as-you-go account spent from the web editor. Pay-as-you-go credits work through the API, the SDK, the CLI and MCP, so token calls never get this code. |
| `api_tokens_payer_only` | | On a deployment with one billing account, only that account can create API tokens. |
| `payer_balance_jwt_only` | | On such a deployment, the billing account read the pool's balance with a token. Only its own browser session may. |
| `payer_required` | | A deployment billing route was called by anything but the billing account's browser session. |

### 404 Not Found

| Code | Meaning |
| --- | --- |
| `not_found` | The resource does not exist, or you cannot see it. The two are deliberately the same answer, so an id never reveals whether something exists. |
| `workspace_not_found` | Work paid by a workspace names a workspace that does not exist. |

### 409 Conflict

| Code | Extra fields | Meaning |
| --- | --- | --- |
| `already_running` | `executionId` | The workflow already has an active run. Poll that run instead. |
| `workflow_conflict` | `currentVersion`, `currentUpdatedAt`, `currentRecord` | The workflow changed since the version you sent. Merge onto `currentRecord` and save again. |
| `workspace_archived` | | You tried to create or move work into an archived workspace. |
| `workspace_has_no_default_project` | | A create in a workspace named no project, and the workspace has none. Name a project. |
| `move_blocked` | | The work was created for an assignment and cannot be moved. |
| `production_capability_required` | | A generic workflow save touched a Studio production that needs its own API. |
| `retained_image_in_use` | | A delete would remove protected images, or a production still has jobs using them. Finish or cancel those jobs first. |

### 413, 422 and 429

| Status | Code | Meaning |
| --- | --- | --- |
| 413 | | Your account is over its storage limit. The SDK raises `StorageExceededError` with `limitBytes`. |
| 422 | `job_blocked` | The deployment's job policy refused the generation before it ran. No job was created and nothing was charged. Show `message` to your user as it is, and do not retry the same request. |
| 422 | `upload_blocked` | The deployment's upload policy refused a file before it was stored. Show `message` as it is. |
| 429 | `rate_limited` | The per-token limit of a personal API token. |
| 429 | `rate_limit_exceeded` | Any other limit: per address, per route, or a published app's daily runs. |
| 429 | `too_many_downloads` | You already have 4 video imports running. |

The `422` codes happen only on deployments that register such a policy. See [Rate limits](https://nodaro.ai/docs/developers/api/rate-limits) for the `429` codes.

### 500 and 503

| Status | Code | Meaning |
| --- | --- | --- |
| 500 | `internal_error` | A server error or a failure of a service it depends on. Retry with backoff. |
| 503 | `price_not_configured` | The requested model has no price on this deployment, so the server refuses instead of billing wrongly. Retrying does not help. |
| 503 | `rate_limit_unavailable` | A spending route could not check its rate limit. Retry later. |
| 503 | `provider_unavailable` | The model's provider is not available. |
| 503 | `billing_unavailable` | Usage reporting is not available on this instance yet. |

## When a job fails later

A request that succeeded can still produce a job that fails. The job then carries `error_message`, and for two kinds of failure a structured `error_hint`:

- `{ "kind": "safety-block", "class": "copyright" | "likeness" | "safety", "retried": boolean, "suggestedProvider"?: string }` when a model's safety filter blocked the request.
- `{ "kind": "policy-block", "policyId": string, "reason": string, "hookPoint": "request" | "result" }` when a deployment's policy rejected it.

Both are always refunded, and `credit_status` on the job shows it. Read [Why a job failed](https://nodaro.ai/docs/developers/api/jobs#why-a-job-failed) for what each field means and when to try another model.

## Errors in the SDK

`@nodaro/sdk` throws a typed error for every failed response. Catch the most specific class first:

| Class | Status | Extra fields |
| --- | --- | --- |
| `UnauthorizedError` | 401 | |
| `ForbiddenError` | 403 | `missingScope` when the code is `insufficient_scope` |
| `NotFoundError` | 404 | |
| `InsufficientCreditsError` | 402 | `required`, `available` |
| `StorageExceededError` | 413 | `limitBytes` |
| `WorkflowConflictError` | 409 | `currentVersion`, `currentUpdatedAt`, `currentRecord`. Also used for the Studio `production_busy` conflict. |
| `RateLimitedError` | 429 | |
| `JobBlockedError` | 422 | |
| `StudioOpError` | 4xx | `opIndex`: which operation of a Studio batch was refused |
| `NodaroError` | any | The base class: `code`, `status` and `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
}
}
```

The polling helpers add their own errors, such as `JobFailedError` and `JobTimeoutError`. See [Run a single node](https://nodaro.ai/docs/developers/api/nodes#example-generate-an-image).

## Errors in the CLI

| Exit code | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | Unauthorized, not found, a wrong argument or a network error. |
| `2` | `--watch` ended and the run failed. |
| `3` | `--watch` stopped because the job is held for review. It is not a failure: check back later. |
| `130` | `--watch` ended and the run was cancelled. |

With `--json`, the CLI prints the payload and exits normally instead of using codes 2, 3 and 130, so check `.status` yourself.

## Frequently asked questions

### What does a Nodaro API error look like?

Every error has an HTTP status and the body { "error": { "code": "…", "message": "…" } }. The code is stable, so dispatch on it. The message is for people and can change.

### Which Nodaro API errors should I retry?

Retry 5xx errors and 429 with exponential backoff, for example after 5, 10 and 20 seconds. Do not retry a 4xx error without changing the request, because the same request fails again. 503 price_not_configured is the exception among server errors.

### What does 402 insufficient_credits mean?

The account does not have enough credits to start the run. The error carries required, the credits the run needs, and usually balance. Add credits on Nodaro Cloud, then send the request again.

### What is error_hint on a failed job?

A structured reason attached to a failed job for two kinds of failure, a model's safety filter and a deployment's policy. Read it instead of parsing error_message, for example to offer the suggested model.

### Why does a route answer 403 in_app_only?

The route exists only for the Nodaro web app's own session, for example the Workflow Copilot or stored Webhook Output credentials. API tokens and OAuth tokens cannot call it. Use the workflow endpoints, the SDK or MCP instead.
