Network calls fail in ambiguous ways. A timeout or a dropped connection does not tell you whether the request was processed, and for a request that moves money, sending it again blindly risks paying twice. Idempotency keys make retries safe: retrying a request with the same key returns the original result instead of performing the operation again.
Which endpoints use idempotency keys
Every money-moving POST requires an idempotencyKey field in the request body:
| Endpoint | Creates |
|---|---|
POST /v1/outbound-payments | An outbound payment |
POST /v1/internal-payments | A transfer between two of your own accounts |
FX trades work the same way but use a different field. POST /v1/fx/trades takes an internalReference, and one reference books at most one trade: retrying with a reference that already booked an identical trade returns 200 with the existing trade instead of a new 201. The same 50-second rule applies to a trade's internalReference.
All other endpoints are naturally safe to retry. Reads (GET) never change state, DELETE /v1/payees/{id} and POST /v1/outbound-payments/{id}/cancel converge to the same end state however many times they are called, and POST /v1/payees and POST /v1/fx/quotes are protected by their own duplicate checks.
Key requirements
- A string of up to 100 characters, generated by you.
- Must be unique per logical operation, not per HTTP attempt. One payment = one key, however many times you need to send it.
- We recommend a UUID v4 generated when the operation is first created in your system, stored alongside it.
Do not derive keys from business fields such as invoice numbers alone. If a legitimate second payment for the same invoice is ever needed, a business-derived key would silently block it.
How it works
- The first request with a given key is processed normally and its outcome is recorded against the key. You receive
201with the created resource. - A retry with the same key and the same request details does not repeat the operation. You receive
200with the original resource's id, so a caller whose create response was lost can recover it simply by retrying. - A request with the same key but different request details is rejected with
409 Conflict. This protects you from accidentally reusing a key for a different operation. - Concurrent requests with the same key are processed once; the duplicates receive the outcome of the winning request.
The 409 problem document carries the id of the resource already created under that key: existingPaymentId on POST /v1/outbound-payments, and existingInternalPaymentId on POST /v1/internal-payments. The field is null when the duplicate was detected by content rather than by key, or while the original request is still in flight; retry once more after the original has committed and the id is returned.
Sending the same payment twice on purpose
Duplicate protection works on content as well as on keys, so a byte-identical resubmission under a new idempotencyKey is still rejected with 409. When you genuinely need to send the same payment again, increment the copyNumber field to signal that the repeat is deliberate. It defaults to 1 and must be 1 or greater. copyNumber is available on both POST /v1/outbound-payments and POST /v1/internal-payments.
Recommended retry pattern
key = uuid4() # generated once, stored with the operation
loop with exponential backoff (for example 1s, 2s, 4s, capped):
response = POST /v1/outbound-payments { ..., "idempotencyKey": key }
if response is 201: done, store the returned id
if response is 200: the operation already happened, store the returned id
if response is 409: stop, investigate key reuse or an unintended duplicate
if response is 401: refresh token + DPoP proof, retry
if response is 429: wait Retry-After, retry
if response is 5xx or timeout: retry with the same key
otherwise (4xx): stop, fix the requestEvery attempt needs a fresh DPoP proof, and on money-moving endpoints a freshly generated signature: created must be within 60 seconds of server time, so a signature cannot be reused between attempts. The request body, and therefore the idempotencyKey, stays byte-identical.
Two rules matter most:
- Never change the key between retries of the same operation. A new key on retry is how duplicates happen.
- Never reuse a key for a different operation. You will get a
409and the second operation will not be performed.
Key retentionA key is kept indefinitely once its operation has completed. A key whose operation did not complete (for example the request failed a business rule after the key was reserved) is released automatically after 50 seconds; retrying within that window returns 409 Duplicate idempotency key in flight. Wait and retry with the same key rather than minting a new one.
Verifying an ambiguous outcome
If you exhaust retries and still do not know the outcome, query for it instead of sending again:
GET /v1/outbound-payments/{id}if you received an id before the connection dropped, orGET /v1/outbound-payments?createdFrom=...and match on your reference.GET /v1/internal-paymentsandGET /v1/fx/tradessupport the same pattern.