New France e-invoicing mandate goes live September 2026 — our implementation is ready. See all mandates →
Guides

Idempotency keys

Every POST request across every Clearvo product must include an x-idempotency-key header containing a UUID v4. This protects against duplicate submissions caused by network timeouts or retries — the example below uses POST /invoices, but the same mechanism applies to every write endpoint.

Guides

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. Generate a new UUID.
  • Keys expire after 24 hours on Starter, 72 hours on Growth, and configurable on Enterprise.

Best practice

1
Generate the key before the request

Use crypto.randomUUID() (Node 19+) or a UUID library. Store it alongside your pending record so you can replay it on retry.

2
Retry on network failure

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.

3
Never reuse a key for a different request

Each unique operation (e.g. a different invoiceNumber, or any field change) must use a fresh UUID.

JavaScript — Idempotent submit with retry
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/invoices', {
        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)));
    }
  }
}