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.
Integrators implement an HTTP(S) endpoint on their side. Esnad POSTs JSON when invoice status changes. Delivery logs and manual retries are available in the dashboard.

Overview

  • Configure a single webhook URL per tenant in the dashboard (use HTTPS in production).
  • We send a POST with a JSON body for each event.
  • Every request includes X-ZATCA-Event (event type) and X-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

EventWhen
invoice.queuedInvoice signed and accepted; ZATCA submission is queued (API may have returned HTTP 202).
invoice.reportedSimplified invoice (or credit/debit note) reported to ZATCA (B2C). Check data.zatca_compliant — if false, ZATCA accepted with warnings; do not resubmit.
invoice.clearedStandard invoice (or credit/debit note) cleared by ZATCA (B2B). Same compliance fields as reported — accepted ≠ fully compliant when warnings are present.
invoice.failedSubmission permanently failed after retries or a non-retryable validation error (see data.error).
webhook.testTriggered by "Send test event" in the dashboard.
cert.expiringEGS 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));
}