We use cookies to improve your experience and analyze site traffic. By clicking “Accept”, you agree to our use of cookies.

Learn more about our privacy practices
Corvo
Português

Async issuance

How issuance processing, tracking and idempotent retries work.

POST /v1/invoices responds immediately: a 202 with the invoice in an early status (queued, then processing/pending) means the request was registered and queued — not that the fiscal authority authorized it. Actual processing is asynchronous.

Tracking

  • Poll GET /v1/invoices/:id and follow status/stage until authorized (or a terminal failure).
  • Fetch the transition history with GET /v1/invoices/:id/events.
  • Subscribe to webhooks (nfse.invoice.authorized, nfse.invoice.documents_ready, etc.) to avoid polling.
  • Check GET /v1/billing/usage (canEmit) before issuing again.

Idempotency-Key

The Idempotency-Key header is optional on issuance and email requests, but recommended — it makes retries safe. Use a stable key per business operation (e.g. the order id in your system) and keep the same key and the same payload on every retry.

The same key, account and environment with content equivalent to the computed hash recovers the same invoice; different content returns 409 (ERROR_IDEMPOTENCY_CONFLICT). The hash is computed before fiscal normalization, with canonical JSON keys and the competence date — do not change how fields are represented between retries (a CNPJ with and without punctuation can produce different hashes).

javascript

const key = `pedido_${pedidoId}_emissao`; // stable per business operation

async function issueWithRetry(buildBody, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`${BASE_URL}/invoices`, {
      method: "POST",
      headers: {
        "X-API-Key": process.env.CORVO_API_KEY,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(buildBody()),
    });
    if (res.status === 409) {
      // ERROR_IDEMPOTENCY_CONFLICT — payload differs; fix the data, don't retry
      throw new Error("Idempotency conflict: payload changed between retries");
    }
    if (res.status < 500) return res.json(); // success or client error — no retry
    await new Promise((r) => setTimeout(r, 2000 * 2 ** i)); // backoff on 5xx
  }
  throw new Error("Issuance request failed after retries");
}