Idempotency
How to retry safely — natural idempotency on the catalogue, and the Idempotency-Key header on payment writes.
Networks fail. A request can succeed on Localoy's side while your server never sees the answer. The Open Network API is built so that you can retry without doing the work twice.
Catalogue writes#
Catalogue writes are idempotent by their natural key, the externalId:
| Call | Repeated |
|---|---|
POST /catalog/items | The first call creates the item (201); a repeat updates it (200). |
PATCH /catalog/items/{externalId} | Setting the same values again leaves the item the same. |
DELETE /catalog/items/{externalId} | The second delete answers 404 — treat that as "already gone". |
The catalogue routes do not use an Idempotency-Key header.
Payment writes#
POST /payments/sessions/{id}/result and
POST /payments/sessions/{id}/refund accept an optional
Idempotency-Key header of up to 255 characters.
Idempotency-Key: 7d1f0b3e-2c4a-4b9e-8f51-0a6c2d9e4b17A repeat with a key already used on the same session is not applied again. It answers 200 with
"replayed": true and the session as it is now.
Even without a key, reporting the status a session already has is a no-op that answers 200 with
"replayed": true.
- Your server → Localoy: POST …/result Idempotency-Key: K1
- Note: Recorded — the order is paid
- Note: The response is lost: network timeout
- Your server → Localoy: Same request, same key K1
- Localoy → Your server: 200 replayed: true — not applied twice
- — Another session: a refused request, corrected —
- Your server → Localoy: POST …/result amountMinor: 9000, key K2
- Localoy → Your server: 409 payment_amount_mismatch
- Your server → Localoy: Corrected body with a NEW key K3
- Localoy → Your server: 200 replayed: false — recorded
Use a new key for a corrected request
An Idempotency-Key is remembered with every attempt, including refused ones, and it is not
compared with the request body. If a result is refused — say 409 payment_amount_mismatch — and you
fix the body but resend it with the same key, the API answers 200 with "replayed": true and
applies nothing. The order stays unpaid.
Reuse a key only to retry the identical request after a timeout or network error. Generate a new
key for every new or corrected request, and always check data.status in the response.
A safe retry loop#
import { randomUUID } from "node:crypto";
async function reportResult(sessionId, body) {
const idempotencyKey = randomUUID(); // one key per logical request
for (let attempt = 1; attempt <= 5; attempt++) {
try {
const res = await fetch(
`${process.env.LOCALOY_BASE_URL}/payments/sessions/${sessionId}/result`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LOCALOY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
},
);
if (res.status < 500 && res.status !== 429) return await res.json();
} catch {
// network error: fall through and retry with the same key
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
throw new Error("Could not report the payment result");
}