Error handling
The HTTP status codes, error envelopes, and retry semantics you'll encounter when integrating with the Nora API.
Nora uses conventional HTTP status codes and returns JSON error bodies on every non-2xx response. Branch on status first, then on the appropriate error envelope for actionable details.
Status codes
| Status | Meaning | What to do |
|---|---|---|
200 / 201 | Success | Parse body. |
400 | Bad request — a structural validation failure (validator envelope, error[].path names each failing field) or a rejected business rule (message slug) | Fix the payload. |
401 | Missing, invalid, revoked or expired key (code: INVALID_API_KEY / API_KEY_EXPIRED) | Fix your X-API-Key configuration. Do not retry. |
403 | Valid key, not allowed here: the key belongs to the other environment, or the request came from an IP outside the key's allowlist | Do not retry; fix the host or the allowlist. |
404 | Resource does not exist in this key's environment | Check the id and that you are on the right host for the key. |
409 | Conflict — state precondition failed (for example, an idempotency-key reused with a different body) | Read the latest resource state and decide whether to proceed. |
422 | On-chain verification failed on a wallet submit/sync | Read details.code. |
429 | Rate limited | Respect Retry-After. See Rate limits. |
5xx | Server error | Safe to retry with backoff. Keep the same idempotency-key and body for an idempotent operation. |
Response envelopes
{
"code": "BAD_REQUEST",
"message": "evm_destination_wallet_not_authorized",
"details": { }
}codeandmessageare present on every non-2xx response that is not a structural validation failure; for business rulesmessageis a stable slug.detailsis optional and endpoint-specific.
Structural validation failures (a missing or mistyped field, a missing
required header) use a different, validator-generated shape: no code, no
message, an error array with one entry per problem and the offending
path, and a data echo of what you sent (empty for header failures):
{
"data": { "amountCents": 100, "destinationWallet": "…", "chainId": "solana" },
"error": [
{ "code": "too_small", "path": ["amountCents"], "minimum": 500,
"message": "amountCents must be at least 500 (R$5.00 = 5 BRS minimum)" }
],
"success": false
}Branch on the HTTP status first; on a 400, check for success === false
and read error[].path / error[].message. Do not log data: it echoes
the body you sent, which on an offramp includes a PIX key. The code/message envelope
covers every other error, including business-rule 400s.
Wallets family (/v2/wallets/*)
{
"code": "CONFLICT",
"message": "The prepared transaction has expired.",
"details": { "code": "TRANSACTION_EXPIRED" }
}codeandmessageare always present, except on structural validation failures, which use the validator shape above.detailsis optional and contains endpoint-specific context.- For wallet-specific errors,
details.codecontains the uppercase value listed in the Wallets reference.
Branching on the error body
On wallet endpoints, inspect response.details?.code for the wallet-specific
code. Use response.code for the general error category and
response.message for diagnostics. Use your own copy for end users.
codeis the HTTP class,messageis the reason.codetakes the upper-case values listed on the Errors page; the business-rule slugs live inmessageand are documented next to each endpoint. Branch on HTTP status first, then onmessagefor the specific failures you want to special-case.
Retry semantics
Not every error is retry-safe:
- Safe to retry with the same idempotency key and body:
5xx, transport failures, and connection resets on intent creation, wallet creation, or wallet submission. The server replays the original response. - Safe to retry with a new idempotency key: time-sensitive rebuilds
— a wallet submission after
details.code: TRANSACTION_EXPIRED. Callprepare, sign the new transaction, and use a new key. - Safe to retry with a new signature: burn signing, where a
stale-blockhash failure before broadcast means you fetch a fresh payload
and re-sign; the intent is still
awaiting_burn_approval. - Do not retry, fix first:
400,401,403,422. - Read first, then decide:
404,409. These often mean the state you're acting on has moved — fetching the latest resource tells you what to do next.
Gotchas
401is not a "try again" signal. The credential itself is the problem; no retry will fix it. Check theX-API-Keyvalue and whether it matches the environment you're calling.422is wallets-only. Intents report rejected business rules as400with a slug inmessage;422is the wallet endpoints' receipt-verification failure. Branch onmessageon intents, ondetails.codeon wallets.- Error bodies are NOT Nora's typed happy-path shape. If you only care about the typed response for 2xx, you still need a fallback path for errors — the envelope is separate and varies by family.
- Don't consume the response body twice. If your HTTP client auto- parses non-ok responses, don't re-read the stream.
See also
- Authentication — fixing
401
- Idempotency — how retries stay safe
- Wallets — wallet machine codes and recovery actions
- Rate limits — handling
429
Idempotency
Retry intent creation and wallet registration safely. Intent creation accepts an idempotency-key header. Wallet create and submit require it. Burn approval takes one proof per intent and no key.
BRS token
How BRS works across chains — decimals, chainId and networks, whitelisting, brand assets and adding a chain.