nora

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

StatusMeaningWhat to do
200 / 201SuccessParse body.
400Bad request — a structural validation failure (validator envelope, error[].path names each failing field) or a rejected business rule (message slug)Fix the payload.
401Missing, invalid, revoked or expired key (code: INVALID_API_KEY / API_KEY_EXPIRED)Fix your X-API-Key configuration. Do not retry.
403Valid key, not allowed here: the key belongs to the other environment, or the request came from an IP outside the key's allowlistDo not retry; fix the host or the allowlist.
404Resource does not exist in this key's environmentCheck the id and that you are on the right host for the key.
409Conflict — state precondition failed (for example, an idempotency-key reused with a different body)Read the latest resource state and decide whether to proceed.
422On-chain verification failed on a wallet submit/syncRead details.code.
429Rate limitedRespect Retry-After. See Rate limits.
5xxServer errorSafe 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": { }
}
  • code and message are present on every non-2xx response that is not a structural validation failure; for business rules message is a stable slug.
  • details is 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" }
}
  • code and message are always present, except on structural validation failures, which use the validator shape above.
  • details is optional and contains endpoint-specific context.
  • For wallet-specific errors, details.code contains 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.

code is the HTTP class, message is the reason. code takes the upper-case values listed on the Errors page; the business-rule slugs live in message and are documented next to each endpoint. Branch on HTTP status first, then on message for 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. Call prepare, 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

  • 401 is not a "try again" signal. The credential itself is the problem; no retry will fix it. Check the X-API-Key value and whether it matches the environment you're calling.
  • 422 is wallets-only. Intents report rejected business rules as 400 with a slug in message; 422 is the wallet endpoints' receipt-verification failure. Branch on message on intents, on details.code on 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

On this page