Networks fail in the worst possible way: your request arrives, we process it, and the
response is lost on the way back. You don’t know whether the payment went out. Retry and
you might send it twice; don’t retry and you might not have sent it at all.
An idempotency key removes that ambiguity. You pick a key, send it with the request,
and reuse the same key on every retry of that same operation. We guarantee the operation
happens at most once — a retry returns the original result instead of creating a second
payment, payee, or card.
Optional today, required later. The Idempotency-Key header is optional right now:
requests without one behave exactly as they always have. We intend to enforce it on all
money-movement and resource-creating endpoints in a future release, so requests without a
key will start being rejected.Send a key on every create from now on. Adding it later is a migration; adding it today is
one line, and it protects you against duplicates in the meantime.
How to send it
Pass the key in the Idempotency-Key request header. It must be a UUID.
Every endpoint in the table below also accepts the key as an idempotencyKey field in the
JSON body, for parity with batch payments. If you send both, the
header wins. Prefer the header — it’s the same on every endpoint and it doesn’t
interact with the request body.
Generate one key per logical operation, not per attempt. Mint the UUID when the user
taps “Send”, store it alongside the operation, and reuse it for every retry — including
retries after a timeout, a 5xx, or an app restart. A fresh key on each attempt gives you
no protection at all.
Where it’s supported
Read-only endpoints (GET) and endpoints that change existing state (block, cancel,
activate) don’t take a key: repeating them doesn’t create anything.
Card load/unload is not on this list. It accepts an
Idempotency-Key, but only uses it to correlate the balance event — it does not
deduplicate, so a retry moves the money again. Confirm the result with
card transactions before retrying a load or unload.
What happens on a retry
409 IDEMPOTENT_REQUEST_IN_PROGRESS and 409 IDEMPOTENCY_KEY_CONFLICT apply to payments
and cards. Adding a beneficiary is a single synchronous write, so it has no in-progress
state.
A batch key is not a replay key. Unlike the single-resource creates, submitting the
same batch twice with the same idempotencyKey does not return the first batch — it
re-runs the submission against the same batchId, and the payments can go out a second
time. Parameters aren’t compared either, so a reused key with a different list of
payments is not rejected.Treat submit as a one-shot call: if a response is lost, track the
batch to find out whether it landed. Never resend.
Scope of a key
On the single-resource creates (payments, beneficiaries, cards), keys are scoped to the
authenticated customer, so two customers can pick the same UUID without colliding.
Batch payments are the exception. A batch’s identity is derived from the
idempotencyKey alone, not from the customer, so the key must be unique across your whole
integration — not just per customer. Generate a fresh UUID per submission (never a
constant, a template value, or a reused test fixture): two submissions that share a key
land on the same batchId and re-run the submission, which can pay the batch out
twice.
A key never weakens the first request’s step-up. When you
add a payee, the confirmation is
verified before the replay is resolved, so a retry still has to prove it’s the
customer.On payments, a replay short-circuits earlier: once a payment has been dispatched under a
key, a retry with that key returns the original result without re-running the step-up. The
original payment was fully authorised — the replay just hands you back its result, so a
lost response doesn’t cost your customer a second confirmation prompt.
Putting it together
The two rules that make this work: the same key and the same parameters on every
attempt. Change either and you’re making a new request, not retrying the old one.