Receiving webhooks
Outbound request format, event types, retries and signature verification.
Corvo sends a JSON POST to your registered URL. Respond with HTTP 2xx within 10 seconds; enqueue slow work in your receiver.
Request format
- Method:
POST. - Headers:
Content-Type: application/json,X-Corvo-Signature: sha256=<hex>. - Body: a JSON envelope with
id,type,environment,createdAtanddata.
json
{
"id": "evt_identifier",
"type": "nfse.invoice.authorized",
"environment": "test",
"createdAt": "2026-10-10T14:00:00.000Z",
"data": {
"invoiceId": "INVOICE_UUID",
"companyId": "COMPANY_UUID",
"environment": "test",
"status": "authorized",
"documentsStatus": "pending",
"emailStatus": null,
"stage": "authorized",
"accessKey": "ACCESS_KEY",
"nfseNumber": 123
}
}data varies by event; in the fiscal flow it includes the invoice's identification and state. Authorization adds accessKey/nfseNumber, rejection adds errorCode/errorMessage, manual review adds reason, ready documents add accessKey/nfseNumber/documentsReadyAt, and a sent email adds sentAt. The test event carries { "message": "This is a test webhook event" }.
Event types
| Event type | When |
|---|---|
nfse.invoice.authorized | The invoice was authorized; adds accessKey/nfseNumber. |
nfse.invoice.rejected | Authorization was rejected; adds errorCode/errorMessage. |
nfse.invoice.failed | Processing failed. |
nfse.invoice.reconciliation_required | Reconciliation is required. |
nfse.invoice.manual_review | Manual review is required; adds reason. |
nfse.invoice.documents_ready | Documents are ready; adds accessKey/nfseNumber/documentsReadyAt. |
nfse.invoice.email_sent | The notification email was sent; adds sentAt. |
nfse.invoice.email_failed | The notification email failed. |
nfse.invoice.fiscal_discrepancy | invoiceId, status and differences[] (field, local, authorized, delta, kind): difference between Corvo's calculation and the authorized NFS-e, above R$ 0.01 per tax. The authorized XML is not changed. |
nfse.invoice.cancelled | The invoice was cancelled; adds reason. |
nfse.invoice.cancellation_rejected | The Sefin refused the cancellation; the invoice is authorized again; adds errorCode and errorMessage. |
webhook.test | Sent by the test route; carries { "message": "This is a test webhook event" }. |
Signature verification
The signature is the HMAC-SHA256 of the sent JSON body with the configuration's secret, in hex. Verify the raw received body bytes — do not parse and stringify before verifying.
javascript
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(rawBody, signatureHeader, secret) {
if (typeof signatureHeader !== "string") return false;
const match = /^sha256=([a-f0-9]{64})$/.exec(signatureHeader);
if (!match) return false;
const received = Buffer.from(match[1], "hex");
const expected = createHmac("sha256", secret).update(rawBody).digest();
return timingSafeEqual(received, expected);
}Retries and delivery semantics
- The outbox makes up to 5 attempts, with intervals after a failure of 60 seconds, 160 seconds, 15 minutes and 30 minutes; execution depends on the worker.
- Retries preserve the original payload — deduplicate by the event
id. - The receiver may get the same event again; the current signature has no separate verification timestamp, so deduplication on the receiver is required.
- Do not assume a global delivery order.