Idempotency keys
Every POST request across every Clearvo product must include an x-idempotency-key header containing a UUID v4, with one exception: on POST /v1/send the header is optional (see below). This protects against duplicate submissions caused by network timeouts or retries — the example below uses POST /send, but the same mechanism applies to every write endpoint.
How it works
- On the first request with a given key, Clearvo processes the request and caches the response.
- On any subsequent request with the same key and identical payload, the cached response is returned immediately — nothing is re-processed or re-submitted to an authority.
- If you submit a different payload with the same key, Clearvo returns HTTP 409 Conflict on the endpoints that require the header.
POST /sendis the exception, see the next point. Generate a new UUID for each new request. POST /v1/sendworks differently, and the header is optional there. Every document gets a system-generated identity key built from its own fields (documentType+country+invoiceNumber; for a purchase also the supplier tax ID and issue date; if you send one order line per call, the line'slineNumberis part of it too, so lines 1 and 2 of the same order are separate documents), returned in theX-Idempotency-Keyresponse header. That key alone decides whether two requests are the same document, so re-sending an invoice returns the stored result whatever header you send. Re-sending the same invoice number with different content returns409 IDEMPOTENT_BODY_MISMATCH(the document has already been processed), whateverx-idempotency-keyyou send; an invoice that was rejected, needs information, or is held on a tax code is re-run instead. A key you supply is only your retry hint and never blocks a document: reusing it for a different document is processed under that document's own identity, and the response carries the warningIDEMPOTENCY_KEY_REUSED_ACROSS_DOCUMENTSinwarnings[]. A document missing one of those identity fields is no longer refused: it is stored under an identity Clearvo mints (your key, when you send one, is used instead) and the response carries the warningNO_IDENTITY_NO_DEDUP, because a re-send of it cannot be matched. The refused edit of an already processed document (409 IDEMPOTENT_BODY_MISMATCH) is recorded in the stored document's history. Every other write endpoint still requires the header.- Keys expire after 24 hours on Starter, 72 hours on Growth, and configurable on Enterprise.
Best practice
Use crypto.randomUUID() (Node 19+) or a UUID library. Store it alongside your pending record so you can replay it on retry.
If the request times out or you receive a 5xx, retry with the same idempotency key. You will get the cached 200 response if Clearvo already processed the request. On POST /send, a 503 INGEST_UNAVAILABLE means nothing was stored, so retry with the same body. A 503 with Retry-After (including a temporary failure before storage) means retry the same request with the same key. When enabled for your account, a 200 with status: RECEIVED and a PROCESSING_DELAYED warning means the document is stored: do not resend it, Clearvo finishes it.
Each unique operation (e.g. a different invoiceNumber, or any field change) must use a fresh UUID.
async function submitWithRetry(payload, idempotencyKey) {
const MAX_RETRIES = 3;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
const res = await fetch('https://api.clearvo.io/v1/send', {
method: 'POST',
headers: {
'x-api-key': process.env.CLEARVO_API_KEY,
'x-idempotency-key': idempotencyKey, // same key on every retry
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.status === 409) throw new Error('Key reused with different payload');
if (res.ok) return res.json();
if (res.status < 500) throw new Error(`Client error: ${res.status}`);
} catch (err) {
if (attempt === MAX_RETRIES - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
}