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.
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
{ "error": { "code": "rate_limited", "message": "Too many requests. Max 30 per minute." } }codeis a stable slug. Dispatch on it.messageis 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_runningputsexecutionIdbesideerror, and a failed pre-run allowance check putsrequiredandremainingat 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.
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_configureddoes not succeed on retry: the model has no price on this deployment until its operator sets one.503 provider_unavailableon 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_retryablemeans nothing was posted and the same request is safe to send again, but500 publish_failedmeans 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. With reason: "llm_model_unpriced", an LLM model that has no price yet. See A model with no price yet. |
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. |
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. |
advanced_mode_unavailable | advancedMode: true while the model's own provider cannot take requests. Nothing is charged. Turn Advanced mode off to run the model. The same code can arrive as 502 mid-call. It is then refunded unless the provider billed the attempt. |
quote_unsupported | POST /v1/quote for a node type the platform cannot quote. The message lists the types it can. See Run quotes. |
continuation_subset_required | continueFromExecutionId without nodeIds or continueMode. See Continue a run. |
continuation_workflow_mismatch | The run named in continueFromExecutionId ran another workflow. |
continuation_version_mismatch | The run named in continueFromExecutionId ran a published app version of the workflow. |
render_final_node_not_found | renderFinal.renderNodeId names no node of the workflow. |
media_unreadable | A use-case run, such as Tighten Episode, Chapters, Trailer, Clip Pack or Speaker Layout, whose recording could not be read. Check that the link opens and is a video or audio file. Nothing is created or charged. |
media_has_no_audio | A use-case run or its estimate: a recording has no sound to transcribe. The message names the recording. Nothing is created or charged. |
media_too_long | A use-case run or its estimate: a recording, or the Master audio, is longer than 3 hours. The message names which. Nothing is created or charged. |
master_audio_unreadable | A use-case run or its estimate with masterAudioUrl: the Master audio could not be read as audio. Nothing is created or charged. |
render_final_not_a_render | renderFinal.renderNodeId names a node that is not an Apply EDL render. |
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 has a workflow scope and the request is outside it, or the route needs a signed-in session. See Limit a token to some workflows and Routes that need a signed-in session. | |
access_blocked | On Nodaro Cloud and the Business edition, an administrator blocked the account. Every credential of the account is refused: the session, API tokens and OAuth tokens. Its schedules and triggers stop, and its published apps answer 404 not_found. Contact support if you think this is a mistake. | |
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. | |
node_not_available | This deployment does not offer the node, or the option you chose in it, to your account. The message names it. Nothing is charged. Every route that spends credits is checked as the node it runs, whatever its path: POST /v1/suno/generate as suno-generate, a trend run (POST /v1/trends/:slug/runs and its /estimate) as cast-swap, Nodaro Motion, POST /v1/video-director/run as video-director, the Video Director, and POST /v1/pipelines and the pipeline routes that start or continue work as film-pipeline, the film pipeline. See Pipelines. | |
model_not_available | The request would run on a model that this deployment does not offer. That is the model you named, or, on a deployment that restricts models, the default the route runs when you name none: the image default for the aspect ratio, a feature's default language model, or a route's one fixed model. Nothing is created or charged. Name a model that GET /v1/models lists. | |
model_disabled | Nodaro Cloud only. An admin switched this model off. Nothing is reserved or charged. Buying credits does not help: choose another model, or try again when the model is switched back on. | |
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. | |
rights_confirmation_required, rights_authorization_required and the other rights_confirmation_* codes | An import of a link has no content rights confirmation that fits. Nothing was fetched. See Content rights. | |
import_source_disabled | The deployment does not allow imports from the link's site. | |
link_imports_disabled | This deployment turned imports from links to other sites off. Upload the file instead. A file of the deployment's own storage is not refused. See Content rights. | |
imports_blocked | Importing links is blocked on the account, or the account is closed, under the deployment's repeat-infringer policy. The message says until when. See Repeat infringers. |
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. |
continuation_not_found | The run named in continueFromExecutionId does not exist or is not yours. |
409 Conflict
| Code | Extra fields | Meaning |
|---|---|---|
already_running | executionId | The workflow already has an active run, or a Retry of the app run is pending. Poll that run instead. |
continuation_not_completed | The run named in continueFromExecutionId is still going, failed or was cancelled. Only a completed run can be continued without continueMode. | |
continuation_not_failed | With "continueMode": "from-failed": the run did not fail. | |
continuation_superseded | executionId, supersededBy | With "continueMode": "from-failed": a newer continuation of the run failed, so continue that one, or it completed everything that was left. See Continue a failed run. |
continuation_preview_pending | executionId, renderNodeIds, reviewNodeIds, next | With "continueMode": "from-failed": the newest continuation stopped at a Preview review. Send next as the body of a run first. |
continuation_subset_required | With "continueMode": "from-failed": nothing is left to continue. On an app run's Retry: every step completed. | |
app_run_version_changed | appVersion, runVersion | A Retry of an app run made on another app version. Start the run over. |
app_run_version_unavailable | A Retry of an app run whose version is no longer available. Start the run over. | |
workflow_conflict | currentVersion, currentUpdatedAt, currentRecord | The workflow changed since the version you sent. Merge onto currentRecord and save again. |
job_cancelled | A synchronous metered LLM request whose job was cancelled after its credits were held, but before its call was sent. See Metered LLM requests. | |
idempotency_key_reused | — | A Tighten Episode run or its continuation (POST /v1/tighten-episode, POST /v1/tighten-episode/continue): the Idempotency-Key already belongs to one of your jobs of another kind, such as an image generation. Nothing is created or charged. Send a new key. A key that belongs to a run of the same node answers that run, with 202 and "deduped": true. |
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. | |
content_held | The file is held as evidence for a complaint, so it cannot be deleted right now. See Restricted content. | |
not_applicable | On a deployment where one billing account pays for every user, a free signup credit route: POST /v1/credits/claim-signup-grant and the two welcome offer routes. Free signup credits do not apply there, so nothing is recorded or granted. There is nothing to retry, and a client that claims on every page load can ignore it. |
413, 422, 429 and 451
| Status | Code | Meaning |
|---|---|---|
| 413 | Your account is over its storage limit. The SDK raises StorageExceededError. | |
| 422 | job_blocked | The deployment's job policy or content guard 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. |
| 422 | offsets_not_applied | POST /v1/edit-plan received a raw offsets field. Write each offset onto its source as sources[].offsetMs first. See Edit Plan. |
| 422 | master_offset | POST /v1/edit-plan received an offset other than 0 on the master, or on the source that the transcript was made from. Nothing is charged. |
| 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. |
| 451 | content_restricted | GET /v1/download or GET /v1/image-proxy was asked for a file that Nodaro restricted. See Restricted content. |
job_blocked and upload_blocked happen only on deployments that register such a policy. See 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. |
| 502 | advanced_mode_unavailable | An Advanced-mode LLM call was refused mid-call by the model's own provider. The hold is refunded unless the provider billed the attempt. Turn Advanced mode off to run the model. |
| 503 | provider_key_missing | A use-case run or its estimate that transcribes, on a self-hosted install with no key for that transcription provider and no Nodaro Cloud connection that covers it. The message names the key. Nothing is created. |
| 503 | job_start_failed | A synchronous metered LLM request could not record that its call is starting. See Metered LLM requests. |
| 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, or billing is briefly unavailable for a workspace run of an app's Retry. Try again shortly. |
Metered LLM requests
A language-model call is charged for the tokens it uses, so its credits are held before the call starts. A synchronous request of this kind, such as Generate Text, Prompt Helper or POST /v1/llm/structured, can answer two more codes:
409 job_cancelled. The job was cancelled after its credits were held, but before its call was sent. The call is never sent, and the hold is refunded. Do not retry the request as it is: the job was cancelled on purpose.503 job_start_failed. The server could not record that the call is starting, so it did not send the call. The hold is refunded. Retry the request.
A model with no price yet
An LLM node is charged per model, at the rate of the lane that serves it. A model with no rate on that lane cannot be run. Every route that would price or run it answers 400:
{
"error": {
"code": "validation_error",
"reason": "llm_model_unpriced",
"modelId": "<model>",
"message": "The model \"<model>\" has no price yet, so it cannot be run. Choose another model."
}
}- Estimates:
POST /v1/credits/estimate-workflow,GET /v1/credits/model-cost,POST /v1/component/estimate-credits,POST /v1/workflows/:id/render-final/estimateand the/estimateroute of each use-case node.POST /v1/credits/model-costsanswers per key instead: it lists the key inerrors, with anerrorCodesentry. - Publishing:
POST /v1/apps/publishandPOST /v1/templates/publishrefuse before anything is written. A republish leaves the current version live. - Runs:
POST /v1/workflows/:id/run,POST /v1/app/:slug/run,POST /v1/app/:slug/runs/:runId/resume,POST /v1/component/execute,POST /v1/api/run,POST /v1/present/:token/run, and the run and continue routes of each use-case node. The check runs before the run is created, with your input overrides applied. No node runs, so nothing is charged. The MCP toolsrun_app,run_workflowandrun_componentreach the same routes. - Triggers: a webhook fire answers the same
400. Schedule, Telegram and plugin-trigger fires are skipped. Each trigger leaves one failed execution witherror_message: "llm_model_unpriced"in the workflow's history, and repeated fires add no more. - Inside the run: the server checks again when a run starts, before any node runs. The check covers the sub-workflows and components the run executes. A run refused here ends
failed, with the message above as itserror_message. A model inside a sub-workflow addsFound inside Sub-workflow node <id>., and one inside a component addsFound inside Component node <id>.
Two read-only routes still describe the workflow. GET /v1/present/:token leaves out estimatedCost and returns estimatedCostUnpriced: { reason, modelId, message }. GET /v1/api/schema leaves out estimatedCredits and returns estimatedCreditsUnpriced in the same shape.
When a job fails later
A request that succeeded can still produce a job that fails. The job then carries error_message, and for five kinds of failure a structured error_hint:
{ "kind": "safety-block", "class": "copyright" | "likeness" | "safety", "retried": boolean, "suggestedProvider"?: string, "subject"?: "input_video" | "input_image" | "prompt" | "audio" }when a model's safety filter blocked the request.subject: "input_video"means the provider refused the job's input video itself, so send a different clip.subject: "input_image", always withclass: "copyright", means the provider refused one of the job's input images as possibly copyrighted: send a different picture, because rephrasing the prompt will not help.subject: "prompt"means the provider's own words named the prompt, so change the prompt.subject: "audio"means the provider's own words blamed the generated audio, not the picture. Treat asubjectyou do not know as absent.{ "kind": "policy-block", "policyId": string, "reason": string, "hookPoint": "request" | "result" }when a deployment's policy rejected it.{ "kind": "credits", "code": string }when the job ran out of credits partway, for exampleinsufficient_creditsor a workspace limit such asbudget_exceeded.{ "kind": "input-refused", "subject": "input_video" }when the provider refused the job's input video without naming a content-policy class. No other model is suggested. Send a different clip.{ "kind": "request-refused", "subject"?: "input_video" }when the provider refused the request as it was sent, naming a fault of it: a setting, a file's format or size, or the prompt's length. The same request is refused again, so change the input images, the video, the prompt or the settings.subject: "input_video"is there when the provider's words name the input video as the fault, such as its length or size, or no character found in it, so send a different clip. It is not a content-policy block.
A failed job also carries error_detail, the provider's own words, or null. The words go only to a caller who can open the run's workflow, and a deployment can keep them for its administrators. See The provider's own words.
The first three are refunded, and credit_status on the job shows it. A failed metered LLM call is the exception: usage the provider billed is charged. Read 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 |
ModelUnavailableError | 403 | A subclass of ForbiddenError for model_not_available and model_disabled: the model is refused, not the caller |
NotFoundError | 404 | |
InsufficientCreditsError | 402 | required, available. code is the refusal's own 402 code. |
CreditCapExceededError | 402 | A subclass of InsufficientCreditsError for budget_exceeded, member_cap_exceeded and user_allowance_exceeded: a cap someone else set |
StorageExceededError | 413 | reason: storage_limit_exceeded for a full storage, file_too_large for one file over the limit |
WorkflowConflictError | 409 | currentVersion, currentUpdatedAt, currentRecord. Also used for the Studio production_busy conflict. |
AlreadyRunningError | 409 | executionId: the run under way, for already_running |
AppRunVersionChangedError | 409 | runVersion, appVersion, for app_run_version_changed |
RateLimitedError | 429 | |
JobBlockedError | 422 | |
StudioOpError | 4xx | opIndex: which operation of a Studio batch was refused |
NodaroError | any | The base class: code, status, message and details, the envelope's other fields |
import {
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.
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 five kinds of failure, a model's safety filter, a deployment's policy, running out of credits partway, a refused input video and a request the provider refused as it was sent. Read it instead of parsing error_message, for example to offer the suggested model.
What does llm_model_unpriced mean?
The workflow or request names an LLM model that has no price yet on the lane it would run on, so it cannot be run. The answer is 400 validation_error with reason llm_model_unpriced and the modelId. Choose another model. Nothing is charged.
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.
Related
Jobs
Rate limits
Authentication
Credits
TypeScript SDK
Last updated on
Credits
Read your Nodaro credit balance and history, list plans and prices, price models and runs before you start them, and learn how reservations and refunds work.
Rate limits
A Nodaro API token allows 30 run requests per minute by default, up to 120. See the per-route limits, the two 429 codes, batch sizes and how to back off.