Invoice V2 webhooks and certificate expiry

Register one callback for all EGS units in an environment. AVTax posts reporting updates to that exact URL. Use egsUnitId in the event to route it to the correct company or checkout.

Configure webhooks · Partner API reference

Set up your receiver

  1. Choose Production or Simulation and save a public HTTPS callback URL, such as https://partner.example.com/webhooks/avtax. Leave Enable invoice notifications unchecked until you finish testing.
  2. Save the signing secret shown once and configure verification on your server. Production and simulation have different secrets, even if they use the same URL.
  3. Send a test notification and inspect delivery history.
  4. Enable invoice notifications. Every current and future EGS unit in that environment is included.

Do not add an EGS ID placeholder to the callback URL. For example, events for egs-shop-a and egs-shop-b reach the same handler; route them using environment and egsUnitId.

Events and invoice state

EventMeaningSequence
invoice.reporting.delayedOne notification when reporting remains unresolved one hour after AVTax first receives the invoice request. The timer uses AVTax server time, not the invoice issue date. Inspect the last recorded problem, next retry time, and whether support is required.1
invoice.reporting.completedA definitive Accepted, AcceptedWithWarnings, or Rejected result.2
webhook.testA synthetic endpoint test. Its EGS ID and invoice data are null; it must not update an invoice.0

Only simplified V2 invoices and their credit/debit/prepayment variants generate reporting events. Both async and wait-for-zatca modes are covered. New Pending/Submitting states do not generate events.

HTTP 200 from invoice submission means the simplified invoice was issued locally. It does not prove successful ZATCA reporting. During temporary errors, we retry the exact signed XML, UUID, and hash. After the initial retry schedule, reporting continues approximately hourly. We never generate a replacement invoice for a retry.

Payload example

{
  "eventId": "678a200b-d27c-48cf-a9af-c21f66a519fa",
  "eventType": "invoice.reporting.delayed",
  "schemaVersion": "1.0",
  "occurredAtUtc": "2026-09-27T10:00:00Z",
  "environment": "production",
  "egsUnitId": "egs-shop-a",
  "sequence": 1,
  "data": {
    "clientRequestId": "checkout-a-invoice-1001",
    "invoiceUuid": "74080a29-8ead-432b-9821-99fab6061f14",
    "documentId": "INV-1001",
    "documentType": "SimplifiedTaxInvoice",
    "invoiceIssuanceStatus": "LocallyIssued",
    "zatcaSubmissionStatus": "Unknown",
    "zatcaSubmissionAttempts": 7,
    "validation": { "status": "UNKNOWN", "info": [], "warnings": [], "errors": [] },
    "nextZatcaRetryAtUtc": "2026-09-27T10:15:00Z",
    "lastProblem": {
      "source": "zatca_response",
      "code": "zatca_http_503",
      "message": "The reporting endpoint returned HTTP 503; no definitive invoice result was received. Reporting will be retried.",
      "httpStatusCode": 503,
      "observedAtUtc": "2026-09-27T09:45:00Z"
    },
    "reportingRequiresAttention": false
  }
}

Events contain status metadata only: no invoice-status URL, signed XML, QR payload, R2 location, or credentials. A transport failure does not prove a ZATCA outage. lastProblem.source distinguishes zatca_response, transport, storage, and credentials_or_configuration. It can be null when no failure has been observed.

If reportingRequiresAttention is true, contact support; automatic authority submission is paused until the prerequisite is resolved. The invoice remains unresolved and its available XML is retained.

Verify, store, acknowledge

  1. Read the exact raw request bytes before parsing JSON.
  2. Verify webhook-signature using HMAC-SHA256 over webhook-id.webhook-timestamp.rawBody. Decode the Base64 part of your whsec_ secret as the key. Signatures use v1,BASE64; during rotation the header may contain two space-separated signatures.
  3. Reject timestamps more than five minutes away from your server clock and compare signatures in constant time.
  4. Store the event durably and deduplicate by eventId, then return any 2xx response within 10 seconds. Duplicate events should also receive 2xx.
  5. Route using environment, EGS unit ID, and client request ID. A completed event may arrive before a delayed event; ignore older sequences for that invoice.

An event may arrive before the original submission HTTP response. Keep your clock synchronized. Body formatting changes invalidate the signature.

Download complete C# receiver (.NET 8+ with Microsoft.Data.Sqlite) · Download complete Node.js receiver (Node.js 22.13+ with node:sqlite).

Both examples verify signatures, persist receipt and an ordered invoice-status projection in SQLite, and acknowledge duplicates safely. Configure WEBHOOK_SECRET and WEBHOOK_ENVIRONMENT for the endpoint's environment; use a separate receiver configuration for the other environment. Run them behind your HTTPS reverse proxy with a persistent database volume. Process further ERP work from the stored inbox.

Retries, retention, and resend

Temporary reporting failures keep the original XML without a deletion deadline. The 24-hour XML deletion countdown starts only after a definitive result. Preserve the returned XML in your own system as its permanent custodian.

Webhook delivery has a separate lifecycle: failed HTTP delivery retries for up to 72 hours, and event payloads/history are retained for 30 days. Redirects are not followed. Delivery failure does not change the invoice result.

Resend notification sends the stored JSON with the same event ID and a fresh signature. It works after the R2 XML is deleted and never resubmits the invoice. Resend cannot extend the original 30-day retention; expired events return 404. Payloads describe the original event time, not the current invoice state.

Disabling an endpoint stops new events and pauses pending delivery; retry windows still expire. Re-enabling resumes unexpired pending deliveries. URL changes affect subsequent attempts. There is no automatic backfill of past completed events.

For the latest state, call GET /api/Invoice/operations/{idempotencyKey} with Api-Version: 2.0 and credentials for the matching EGS unit. Existing authentication rules apply. Polling remains available when webhooks are not configured.

Certificate expiry and new invoice acceptance

New invoice acceptance stops 24 hours before certificate expiry for both V1 and V2. Existing reporting retries are not stopped by this earlier cutoff. Fresh issuance returns HTTP 409 with egs_certificate_renewal_required, the EGS unit ID, expiry, and acceptance cutoff.

Partners receive grouped expiry emails 10, 5, and 2 days before expiry. Each row identifies the company, EGS unit, environment, days remaining, exact expiry, and exact submission cutoff. Display dates use Saudi Arabia time (UTC+03:00). Renewal replaces the reminder schedule for the old certificate.

Renew before the cutoff. If unresolved invoices prevent renewal, contact support. A long outage can leave an already-issued invoice unresolved when its certificate expires; do not recreate the invoice or replace its signed XML.

Partner management API

Use a partner bearer token. The base route is /api/Partner/webhooks/{environment}, where environment is production or simulation.

Method and suffixAction
GET / PUTRead/save URL and enabled state. PUT body: { "url": "https://partner.example.com/webhooks/avtax", "enabled": false }
POST /testQueue a test notification; at most one per minute.
POST /rotate-secretRotate explicitly; the previous secret remains valid for 24 hours.
GET /deliveriesFilter by egsUnitId, status, invoice, fromUtc, toUtc, page, and pageSize.
GET /deliveries/{eventId}Read saved payload and recent delivery attempts.
POST /deliveries/{eventId}/replayResend the retained notification. Enable the endpoint first.