Idempotency
Mobile networks drop responses. An Idempotency-Key makes it safe to retry a request that moves money without ever charging or paying twice.
Every request that moves money requires an Idempotency-Key header:
POST /v1/chargesPOST /v1/payoutsPOST /v1/depositsPOST /v1/withdrawals
Without one, the request is refused with idempotency_key_required. The key is 8 to 255 characters. Your own order or reference id is the right choice: it is already unique per operation, and you have it on hand when you retry.
Idempotency-Key: order-9001What happens on a retry
| You send | SkanPay does |
|---|---|
| A key it has not seen | Processes the request normally. |
| The same key and the same body | Returns the original response and status code, without doing anything again. The response carries Idempotent-Replayed: true. |
| The same key and a different body | 409 idempotency_key_reuse. A key names one operation. |
| The same key while the first request is still running | 409 idempotency_key_in_progress. Retry in a second or two. |
Keys are scoped to your account and to the endpoint, so the same order id can be the key for a charge and, later, for a payout. They are kept for at least 24 hours.
If a request fails before anything is created (a validation error, an unknown number, a limit), its key is released, so a corrected retry with the same key is a fresh attempt. Once a charge exists, the key always returns that charge, whatever its outcome.
A safe retry loop
async function createCharge(order) {
const request = () =>
fetch("https://api.skanpay.website/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SKANPAY_SECRET_KEY}`,
"Idempotency-Key": order.id, // the SAME key on every attempt
"Content-Type": "application/json",
},
body: JSON.stringify({ // the SAME body on every attempt
amount: order.amount,
currency: order.currency,
msisdn: order.phone,
reference: order.id,
}),
});
for (let attempt = 1; attempt <= 4; attempt++) {
try {
const res = await request();
// Retry only what can change on its own: 409 in progress, 429, 5xx.
if (res.status === 409 || res.status === 429 || res.status >= 500) {
const body = await res.json();
if (body.error?.code === "idempotency_key_reuse") return body;
} else {
return await res.json(); // success, or an error to show the customer
}
} catch {
// Network error or timeout: we do not know if it arrived. Retrying with
// the same key is exactly what makes that safe.
}
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
}
throw new Error("SkanPay unreachable; the order stays unpaid until a webhook says otherwise");
}Want to charge the same order again after a genuine failure (the customer declined, then tried again)? That is a new operation: use a new key, for example order-9001-2, and a new reference if you want to tell the attempts apart.