Documentation
Idempotency
The keying strategy that makes retries safe by construction.
Networks fail. Requests time out. Load balancers return a 502 after the work is already committed. Without idempotency, each of those turns into a duplicate payment, and duplicate payments are the most expensive class of bug in this domain.
How it works#
Any mutating request may carry an Idempotency-Key header. JuicePay stores the key alongside the resulting resource for 24 hours.
curl -s -X POST "$JUICEPAY_URL/v1/payouts" \
-H "Authorization: Bearer $JUICEPAY_KEY" \
-H "Idempotency-Key: payroll-2026-08-19-run-2" \
-d '{ "amount": "2000.00", "asset": "USDC", "recipient": "rcp_2xN8" }'- First call — the payout is created and the response is stored.
- Retry with the same key — the stored response is returned verbatim, with
Idempotent-Replay: true. Nothing new is created. - Same key, different body —
409 idempotency_key_reuse. This is a genuine bug in your code and should page someone.
Choosing a key#
The key must come from your domain, not from a random number generator. A random key that regenerates on retry is worse than no key at all, because it looks correct.
Good keys are stable across retries and unique per intent-to-act:
// Stable across every retry of this logical operation.
const key = `payroll:${run.id}:recipient:${recipient.id}`;
await juicepay.payouts.create(
{ amount: recipient.amount, asset: "USDC", recipient: recipient.walletId },
{ idempotencyKey: key },
);The composite shape matters. payroll:${run.id} alone would make only the first payment in a batch idempotent and let the other nine thousand through as new requests.
Batch semantics#
Payout batches take a key for the batch itself. Individual legs are keyed deterministically from the batch key plus the row index, so retrying a partially accepted batch never double-pays the legs that already went through.
{
"batch": "payroll-2026-08-19-run-2",
"legs": [
{ "row": 0, "amount": "2000.00", "recipient": "rcp_2xN8" },
{ "row": 1, "amount": "1850.00", "recipient": "rcp_5tQ1" }
]
}Re-submitting this batch after a timeout returns the existing batch, including which legs succeeded. You never need to reason about which subset landed.
Interaction with webhooks#
Idempotency protects the API boundary; it does not protect your webhook consumer. Deliveries can and do repeat — see Handle webhooks reliably for the consumer side of the same problem.
Retention and expiry#
Keys are retained for 24 hours. Retrying after that window creates a new resource. If your retry interval can exceed 24 hours — a manual finance workflow, for instance — persist the resulting resource ID on your side and check it before re-requesting.
An idempotency key is a promise that the operation is safe to repeat. If the underlying action is not safe to repeat, fix that first; the header will not save you.