Skip to content
JuicePay

JuicePay documentation

Create account

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.

bash
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:

ts
// 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.

json
{
  "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.