Idempotency
How to repeat a request without being charged twice.
If the connection drops in the middle of "generate keys", your system does not know whether the keys came out. Repeating the request without protection would generate and charge again.
That is why Generate keys requires the Idempotency-Key header: a unique code you create for each request. Repeating with the same code, the API returns the same keys and does not charge again.
Idempotency-Key: 3f1c9e2a-7b4d-4c1e-9a0f-5d2b8c6e1a47The code
- A UUID works, and it is the simplest (
crypto.randomUUID(),uuid.uuid4()). - Up to 100 characters: letters, digits and
_ - : .β no spaces or accents. - One code per sale, created before calling the API and stored with it.
What happens when you repeat
| Situation | Response |
|---|---|
| Same code and same request, and the first one has finished | The same keys, without a charge, with "idempotent_replay": true and the Idempotent-Replayed: true header. |
| Same code, and the first one is still running | 409 β wait a few seconds and retry. |
| Same code with a different request (another product, quantity or duration) | 409 β use a new code for a new request. |
| The first one failed (no balance, invalid data, error) | Nothing was charged; the code is free to try again. |
Reuse the code only to repeat the same request. Never create a new code to retry: the API then treats it as another sale and charges again.
Safe retry example
async function generateKeys(order) {
// The code is born with the order and stays stored in it.
order.idempotencyKey ??= crypto.randomUUID();
await saveOrder(order);
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const res = await fetch(`${process.env.PROJECT_API_URL}/keys`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'Idempotency-Key': order.idempotencyKey,
},
body: JSON.stringify({ product_id: order.productId, quantity: order.quantity, days: order.days }),
signal: AbortSignal.timeout(30_000),
});
// 5xx and 409 (still running): wait and retry with the SAME code.
if (res.status >= 500 || res.status === 409) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
if (attempt === 3) throw error;
await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
}
}
}