nora

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.

Network failures, timeouts, and user double-clicks are facts of life. Nora uses the idempotency-key header to make selected write operations safe to retry. Generate one UUID for a logical operation. Reuse that UUID and the same request body for every retry of that operation.

How it works

Loading diagram…

On intent creation, Nora looks the intent up by its reference: the clientReference you send, or the idempotency-key when you send none (it is then echoed back as the intent's clientReference). References are permanent and shared by onramp and offramp within an environment. A second call with the same key and the same body does not re-execute the operation — it returns the existing intent with HTTP 200 instead of the 201 a fresh create answers (intent responses carry no replayed field; wallet responses do). A different body, or a reference that belongs to another intent, is a 409.

Which operations carry a key

The /v2 API uses the idempotency-key header on these endpoints:

  • POST /v2/intents/onramp — mint a fresh UUID per user intent; reuse the same UUID when retrying the same click.
  • POST /v2/intents/offramp — same rule as onramp, with one difference: the replay happens only when the body carries no clientReference. With one, every retry answers 409 Intent with clientReference '…' already exists; the intent exists, page through GET /v2/intents?intentType=offramp and match clientReference (there is no filter for it).
  • POST /v2/wallets — the header is required. Reuse the same UUID and body when retrying one wallet registration.
  • POST /v2/wallets/:id/submit — the header is required. Reuse the same UUID and signed transaction when retrying one submission.

The header is format-validated as a UUID.

clientReference is unique per environment, forever (expired intents included), on both endpoints. A new user operation needs a new reference.

  • POST /v2/intents/:id/approve-burn — no separate idempotency-key header. See Burn-approval nuance.

GET endpoints are already idempotent and don't need the header.

Rule of thumb: one key per logical write

  • Generate a fresh UUID before the first request.
  • Persist the UUID with the request body.
  • Reuse both values after a timeout, connection reset, or application restart.
  • Generate a new UUID when the body changes or the user starts a new operation.

Burn-approval nuance

POST /v2/intents/:id/approve-burn — one proof per intent. If your call timed out, retry with the same proof (txSignature on Solana, txHash on Ethereum): either it lands, or you get 400 Intent is not awaiting burn approval, which means the first call had already succeeded. Never sign a second burn for the same intent. If a Solana blockhash expired before broadcast, rebuild and re-sign from a fresh payload: nothing was burned, and the intent is still awaiting_burn_approval.

Wallet-submission nuance

If POST /v2/wallets/:id/submit times out, retry the same signed transaction with the same idempotency-key. Do not sign a second transaction while the first preparation remains valid.

If the preparation expires, call POST /v2/wallets/:id/prepare. Sign the new transaction and submit it with a new idempotency-key. Reusing the old key with the new signed bytes returns 409 with details.code of IDEMPOTENCY_KEY_MISMATCH.

Wallet create and submit responses include replayed. It is true when the server returns the stored result for an idempotent retry.

Gotchas

  • Keys are scoped per endpoint. The same UUID sent to two different paths is two separate cache entries — you don't need to globally coordinate keys.
  • Keys are scoped per API key (and therefore per instance). Because X-API-Key binds to an (org, instance) tuple, the same UUID reused against sandbox and production is two separate operations on two separate servers' cache entries.
  • Don't generate the key at module load. If your process is long- lived, a module-level randomUUID() would make every user intent share one key — the opposite of what you want. Generate inside the request handler, per user action.
  • There is no expiry. A reference stays taken for as long as the intent exists, including after it expired or failed. A new user operation always needs a new key and, if you send one, a new clientReference.

See also

  • Error handling — duplicate-key replays return the original response, including the original status code
  • Burn signing — one proof per intent, no idempotency-key
  • Wallets — wallet create and submit response schemas

On this page