Every quote-creation route accepts an optional idempotencyKey in the request body. Retrying with the same key and the same payload replays the original Quote instead of creating a duplicate — so a flaky network or a double-clicked submit is always safe.
How it works
The key is unique per Environment together with the quote scope. The server stores a hash of the payload alongside the key:
- Same key, same payload → the original Quote Intent and Receipt are returned (a replay). No new Quote, no duplicated usage event.
- Same key, different payload →
409 with code: "idempotency_conflict". The server refuses to guess which request you meant.
- No key → the server generates one (
auto_<uuid>). The Quote is still safe from accidental double-submits inside the same request lifecycle, but you cannot replay it yourself — always send your own key for any request you might retry.
Key rules
The retry pattern
- Generate one key per logical submission (for example, per checkout attempt).
- Send the request. On a timeout or
5xx, retry with the same key and the same payload.
- On
2xx, store the Quote ID. Any later retry with the same key returns that same Quote.
- On
409 idempotency_conflict, the payload changed under a reused key — treat it as a bug in your key discipline, not as a transient error.
Never reuse a key across edited payloads. If the customer changes an answer, mint a new key for the new submission — otherwise the edit is rejected with 409. Retries reuse keys; edits regenerate them.
Edited submissions
The rule of thumb: one key per payload. The @pricing/widget controller implements this discipline for you — it mints a fresh qi_<uuid> key whenever the offering or any factor changes after the last submit, and reuses the pending key only for a genuine retry of an unchanged payload. Mirror the same behavior in custom integrations:
What idempotency does not do
- It does not deduplicate across different keys. Two different keys with identical payloads create two Quotes.
- It does not make reads consistent —
GET replays nothing; it reads current state (Receipts are immutable, so Quote reads are stable anyway).
- It does not replace error handling. A
422 will fail identically on every replay.