idempotency_key_reused
HTTP 409 type: conflict param: Idempotency-Key{ "error": { "type": "conflict", "code": "idempotency_key_reused", "message": "Idempotency-Key 8f0b4e2a-6c31-4d9f-9a75-2b8e1c0d47f3 was already used for a different request body. Use a new key for a new sale, and the original key only for retries of the original sale.", "param": "Idempotency-Key", "upstream": null, "doc_url": "https://docs.fiskhub.com/errors/idempotency_key_reused" }}What happened
This Idempotency-Key was used within the last 24 hours with a different request body.
Bodies are compared by hash. Same key plus an identical body replays the stored response; same key plus a different body is this conflict. Nothing was processed — the request was refused outright rather than guessing which of the two sales you meant.
Should you retry?
Not as sent. Work out which case you are in first.
The fix
If this is a genuinely different sale
Use a new key. One key per sale, generated when the transaction begins.
The usual cause is a key derived from something that repeats — device_id + total, or a counter
that resets. Two identical €4.50 sales at the same machine are different sales, and a derived key
makes the second one look like a retry of the first. Derive keys from your own transaction id, or
use a UUIDv4.
If this is meant to be a retry of the same sale
The body changed between attempts, and it must not. Common causes:
-
occurred_atrecomputed asnew Date()on each attempt. It must be the original sale moment, fixed when the sale happened, not the moment of the request. -
The body re-serialised per attempt, producing a different key order and therefore a different hash. Serialise once and reuse the string:
// Serialise ONCE, outside the retry loop.const body = JSON.stringify(sale.payload);for (let attempt = 1; attempt <= 3; attempt++) {const res = await fetch(url, {method: "POST",headers: { "Idempotency-Key": sale.idempotencyKey, ...headers },body, // the same bytes every time});// ...} -
A field added or removed between attempts — a
special_purposenote appended on the second try, a rounding applied only on the retry.
Fix whichever it is, then send again with the original key.
Checking whether the first attempt succeeded
The conflict tells you the key has been used, not what happened. To find out, list the invoices for that device around that time:
curl -sS -G "https://api.fiskhub.com/v1/invoices" \ -H "Authorization: Bearer $FISKHUB_API_KEY" \ --data-urlencode "device_id=6f1c0f9e-2b7a-4a56-9d3e-1f0b8a4c7d21" \ --data-urlencode "from=2026-08-27T10:00:00+02:00"If the sale is there, it was fiscalized and no further action is needed.
Lifetime
Records live 24 hours from first use, scoped to your API key and mode. A key reused after that window is treated as new.
See idempotency.