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
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 noclientReference. With one, every retry answers409 Intent with clientReference '…' already exists; the intent exists, page throughGET /v2/intents?intentType=offrampand matchclientReference(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 separateidempotency-keyheader. 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-Keybinds 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
- Authentication — how
X-API-Keyscopes your idempotency-key cache
- 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