Skip to content

Payments

Idempotency and safe retries

Last updated 14 September 2026

A payment request that times out is the worst kind of failure: you do not know whether it happened. Idempotency keys turn that into a safe retry.

How the key works

Every POST /payments must carry an x-idempotent-key header: a UUID v4 that you generate. If Xace receives a second request with the same key, it does not create a second payment. The key identifies the request, so a retry with the same key and the same body is safe.

Generating and storing keys

The key must be created before the first attempt and stored with the thing it represents, so a retry after a crash can find it again.

payout-with-idempotency.jsjavascript
async function submitPayout(payout) {
  // payout.idempotencyKey is created when the payout row is created,
  // not when the request is sent.
  if (!payout.idempotencyKey) {
    payout.idempotencyKey = crypto.randomUUID();
    await db.save(payout);
  }

  const res = await fetch('https://customer-api.prod.xace.io/payments', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'x-idempotent-key': payout.idempotencyKey,
    },
    body: JSON.stringify(payout.body),
  });

  if (res.status === 202) {
    const data = await res.json();
    await db.markSubmitted(payout.id, data.submitted[0]?.xtid, data.requestId);
  }
  return res;
}

Rules:

  1. One key per logical payment. If the amount or payee changes, that is a new payment and a new key.
  2. The same key must always be sent with the same body.
  3. Keys are UUID v4. Do not derive them from the invoice number; a re-issued invoice would collide.

After a timeout

  1. Retry with the same key and body. Usually this simply returns the original result.
  2. If the retry also fails, do not generate a new key. Look the payment up instead: GET /account/{xaid}/approvals for anything awaiting approval, and GET /account/{xaid}/transactions for anything already submitted, matching on reference.
  3. Only if neither shows the payment, and the retries are exhausted, raise it to a human. Do not let the job decide to send again with a fresh key.

A new key is a new payment

Every duplicate payout incident in an integration comes from a retry with a fresh key. Store the key first; reuse it always.

Batches

An array body uses one key for the whole batch. Retrying the batch with the same key is safe. If you need to resubmit only the rejected lines from a batch, that is a new request with a new key and a body containing only those lines.

Was this page helpful?
Suggest edits