Webhooks
Receive invoice lifecycle events at your endpoint. Configure the URL and signing secret in the dashboard (HTTPS in production; HTTP is allowed for local testing). URL and signing secret in the dashboard.
Same compliance fields as the API
invoice.reported / invoice.cleared payloads include data.zatca_compliant, zatca_warnings, and resubmit. If resubmit: false, do not resend the same invoice_number/uuid. See Responses.Overview
- Configure a single webhook URL per tenant in the dashboard (use HTTPS in production).
- We send a
POSTwith a JSON body for each event. - Every request includes
X-ZATCA-Event(event type) andX-ZATCA-Delivery-ID(unique delivery id — use for idempotency). - Optionally set a signing secret; we then send
X-ZATCA-Signature: sha256=<hmac_hex>(HMAC-SHA256 of the raw body). - Retries: Failed deliveries (non-2xx or timeout) are retried automatically up to 5 times with exponential backoff (1m, 5m, 15m, 1h). After that, events are marked permanently failed and can be retried from the dashboard.
Event types
| Event | When |
|---|---|
| invoice.queued | Invoice signed and accepted; ZATCA submission is queued (API may have returned HTTP 202). |
| invoice.reported | Simplified invoice (or credit/debit note) reported to ZATCA (B2C). Check data.zatca_compliant — if false, ZATCA accepted with warnings; do not resubmit. |
| invoice.cleared | Standard invoice (or credit/debit note) cleared by ZATCA (B2B). Same compliance fields as reported — accepted ≠ fully compliant when warnings are present. |
| invoice.failed | Submission permanently failed after retries or a non-retryable validation error (see data.error). |
| webhook.test | Triggered by "Send test event" in the dashboard. |
| cert.expiring | EGS unit certificate expiring within 30/14/7/1 days or expired. |
Invoice events use envelope version api_version: "2026-07-24". invoice.submitted is not emitted (reserved type only).
Payload examples
Amounts in data are SAR numbers. signed_xml_url and pdf_url are API paths (authenticate with your API key). qr_code is present for simplified invoices when signed XML is available. Invoice payloads also include zatca_compliant, zatca_warning_count, zatca_warnings, and resubmit. Event names stay reported/cleared even when warnings exist — check zatca_compliant and resubmit: false (do not resend the same invoice_number/uuid). Do not treat warnings as invoice.failed.
invoice.queued
{
"event": "invoice.queued",
"api_version": "2026-07-24",
"occurred_at": "2026-07-24T10:15:30.000Z",
"tenant_id": "ten_01hxyz…",
"data": {
"invoice_id": "inv_01hxyz…",
"uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-2026-0042",
"status": "queued",
"invoice_type": "standard",
"document_type": "invoice",
"egs_unit_id": "egs_01hxyz…",
"environment": "sandbox",
"reference_uuid": null,
"subtotal": 1000,
"vat": 150,
"total": 1150,
"qr_code": null,
"signed_xml_url": "/v1/invoices/inv_01hxyz…/xml",
"pdf_url": "/v1/invoices/inv_01hxyz…/pdf",
"reported_at": null,
"cleared_at": null,
"report_deadline": null,
"zatca_status": null,
"retry_count": 0,
"next_retry_at": "2026-07-24T10:16:30.000Z",
"error": null
}
}invoice.cleared
{
"event": "invoice.cleared",
"api_version": "2026-07-24",
"occurred_at": "2026-07-24T10:15:45.000Z",
"tenant_id": "ten_01hxyz…",
"data": {
"invoice_id": "inv_01hxyz…",
"uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-2026-0042",
"status": "cleared",
"invoice_type": "standard",
"document_type": "invoice",
"egs_unit_id": "egs_01hxyz…",
"environment": "sandbox",
"reference_uuid": null,
"subtotal": 1000,
"vat": 150,
"total": 1150,
"qr_code": null,
"signed_xml_url": "/v1/invoices/inv_01hxyz…/xml",
"pdf_url": "/v1/invoices/inv_01hxyz…/pdf",
"reported_at": null,
"cleared_at": "2026-07-24T10:15:44.000Z",
"report_deadline": null,
"zatca_status": "CLEARED",
"zatca_compliant": true,
"zatca_warning_count": 0,
"zatca_warnings": [],
"resubmit": true,
"retry_count": 0,
"next_retry_at": null,
"error": null
}
}invoice.reported
{
"event": "invoice.reported",
"api_version": "2026-07-24",
"occurred_at": "2026-07-24T10:16:00.000Z",
"tenant_id": "ten_01hxyz…",
"data": {
"invoice_id": "inv_01habc…",
"uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"invoice_number": "SIM-2026-0099",
"status": "reported",
"invoice_type": "simplified",
"document_type": "invoice",
"egs_unit_id": "egs_01hxyz…",
"environment": "sandbox",
"reference_uuid": null,
"subtotal": 50,
"vat": 7.5,
"total": 57.5,
"qr_code": "AQlTYW1wbGUuLi4=",
"signed_xml_url": "/v1/invoices/inv_01habc…/xml",
"pdf_url": "/v1/invoices/inv_01habc…/pdf",
"reported_at": "2026-07-24T10:15:59.000Z",
"cleared_at": null,
"report_deadline": "2026-07-25T10:15:00.000Z",
"zatca_status": "REPORTED",
"zatca_compliant": true,
"zatca_warning_count": 0,
"zatca_warnings": [],
"resubmit": true,
"retry_count": 0,
"next_retry_at": null,
"error": null
}
}invoice.failed
{
"event": "invoice.failed",
"api_version": "2026-07-24",
"occurred_at": "2026-07-24T12:00:00.000Z",
"tenant_id": "ten_01hxyz…",
"data": {
"invoice_id": "inv_01hfail…",
"uuid": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"invoice_number": "INV-2026-0100",
"status": "failed",
"invoice_type": "standard",
"document_type": "invoice",
"egs_unit_id": "egs_01hxyz…",
"environment": "sandbox",
"reference_uuid": null,
"subtotal": 1000,
"vat": 150,
"total": 1150,
"qr_code": null,
"signed_xml_url": "/v1/invoices/inv_01hfail…/xml",
"pdf_url": "/v1/invoices/inv_01hfail…/pdf",
"reported_at": null,
"cleared_at": null,
"report_deadline": null,
"zatca_status": null,
"retry_count": 12,
"next_retry_at": null,
"error": {
"code": "ZATCA_REJECTED",
"message": "ZATCA rejected the invoice",
"retryable": false,
"zatca_errors": [
{
"zatca_code": "BR-KSA-XX",
"zatca_message": "Example validation message"
}
]
}
}
}webhook.test
Compact payload (no api_version envelope).
{
"event": "webhook.test",
"message": "Test delivery from Esnad API",
"timestamp": "2026-07-24T10:20:00.000Z"
}cert.expiring
Compact payload. alert_type is one of 30_days, 14_days, 7_days, 1_day, expired.
{
"event": "cert.expiring",
"egs_unit_id": "egs_01hxyz…",
"unit_name": "Main POS",
"alert_type": "30_days",
"cert_expires_at": "2026-08-23T00:00:00.000Z",
"days_until_expiry": 30
}Verifying the signature
If you set a webhook secret, verify the X-ZATCA-Signature header using the raw request body (UTF-8) and your secret. Use constant-time comparison to avoid timing attacks. Deduplicate using X-ZATCA-Delivery-ID.
// Node.js example
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}