Webhooks

Webhooks tell your server when a document or a device changes, so your
integration does not need to poll. Wafeq sends each event as a signed HTTPS
POST to the endpoints that you add.

Add an endpoint in the Wafeq e-invoicing portal
(einvoicing.wafeq.com), under
Developer → Webhooks. Select the events that the endpoint receives. The
portal shows the endpoint's signing secret once, when you add it. Sandbox and
Live have separate endpoints, events and signing secrets.

The event names and the body have the same format as Wafeq's ZATCA webhooks,
so one receiver can handle both.

The events

EventWafeq sends it when
nbr.<document_type>.clearedNBR clears a standard document.
nbr.<document_type>.reportedNBR accepts the report of a simplified document.
nbr.<document_type>.errorThe document has validation errors: Wafeq's checks stopped it before NBR, or NBR rejected it.
nbr.<document_type>.failedNBR does not accept the document for another reason, for example because it refuses the request.
nbr.<document_type>.pendingA send to NBR gets no answer. Retry the document to settle it.
nbr.device.activatedA device receives its production certificate, after registration, activation or renewal. It can sign.
nbr.device.revokedA device is deactivated, or replaced by a newer device for its branch. It can no longer sign.

<document_type> is one of invoice, simplified_invoice, credit_note,
simplified_credit_note, debit_note, simplified_debit_note,
prepayment_invoice and prepayment_simplified_invoice. The status in the name
is the document's status from
GET /{family}/{id}, in lower case.

Wafeq sends a document event for each outcome that it records. A document that
you retry can therefore produce more than one event, also with the same status.

Your organization receives the events of its connected accounts' documents and
devices, and of its own provider device. A new endpoint does not receive past
events. Use the list endpoints to read earlier documents.

The format of an event

The body is a JSON object with these fields:

FieldValue
event_idThe ID of the event (evt_…). It is the same value as the webhook-id header.
event_typeThe name of the event, for example nbr.invoice.cleared.
payloadFor a document event, the document exactly as GET /{family}/{id} returns it. For a device event, the device as GET /devices returns it, plus deactivation_reason.
timestampThe time when Wafeq created the event.
testPresent and true only in a test event.

Example of an nbr.invoice.cleared event:

{
  "event_id": "evt_0b7e1c2d3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
  "event_type": "nbr.invoice.cleared",
  "payload": {
    "status": "CLEARED",
    "created_ts": "2026-10-28T10:20:41.512031+00:00",
    "sent_ts": "2026-10-28T10:20:41.512031+00:00",
    "reported_ts": "2026-10-28T10:20:41.512031+00:00",
    "document": {
      "id": "6578731b-a682-52b3-8f42-9be36746f52f",
      "document_number": "INV-1001",
      "issue_datetime": "2026-10-28T10:20:40+00:00",
      "supply_date": null
    },
    "reference": "PO-4471",
    "metadata": {},
    "qr_code_data": "AQ1XYWZlcSBUcmFkaW5n…",
    "validation_status": "PASS",
    "clearance_status": "CLEARED",
    "reporting_status": null
  },
  "timestamp": "2026-10-28T10:20:42.004417+00:00"
}

Process an event

Wafeq sends each event at least once, so your server can receive the same event
more than once. Events can also arrive in a different order than the changes.

StepAction
1Check the signature over the raw body.
2Find duplicates with the webhook-id header.
3Store the event, then answer with a 2xx status.
4Order the events of a document or device by timestamp.

Step 1: Check the signature

Each request has three headers in the Standard Webhooks
format:

  • webhook-id: the ID of the event.
  • webhook-timestamp: the time of this delivery attempt.
  • webhook-signature: the signature of the ID, the timestamp and the body.

Check the signature with a Standard Webhooks library and the endpoint's signing
secret (whsec_…). Use the raw bytes of the body: parsing the JSON and
serializing it again changes the bytes.

from standardwebhooks.webhooks import Webhook

event = Webhook(signing_secret).verify(raw_body, request_headers)
import { Webhook } from "standardwebhooks";

const event = new Webhook(signingSecret).verify(rawBody, requestHeaders);

The library also checks the timestamp, which stops old requests that someone
sends again. If the check fails, do not process the request.

Step 2: Find duplicates

If your server already stored the webhook-id, answer with a 2xx status and
do nothing more.

Step 3: Store, then answer

Store the event, answer with a 2xx status, and process the event afterwards.
If your server does not answer with a 2xx status in time, Wafeq sends the
event again.

Step 4: Order the events

Use timestamp to order the events of one document or device. Do not let an
older event replace newer data. To read the current state, call
GET /{family}/{id}.

Retries

A delivery attempt fails when your server answers with a status of 400 or
higher, does not answer in time, or cannot be reached. Wafeq then retries up to
10 times, doubling the wait each time (times are approximate):

AttemptTime after the previous attemptTime after the first attempt
Retry 130 seconds30 seconds
Retry 21 minute1.5 minutes
Retry 32 minutes3.5 minutes
Retry 44 minutes7.5 minutes
Retry 58 minutes15.5 minutes
Retry 616 minutes31.5 minutes
Retry 732 minutes1 hour 4 minutes
Retry 81 hour 4 minutes2 hours 8 minutes
Retry 92 hours 8 minutes4 hours 16 minutes
Retry 104 hours 16 minutes8 hours 32 minutes
  • A retry has the same body and webhook-id as the first attempt, and new
    webhook-timestamp and webhook-signature headers.
  • Each endpoint retries on its own: a failing endpoint does not delay the others.
  • After the last retry, the event is not lost. Send it again from the portal:
    in Developer → Events, open the event, select Delivery attempts, then
    Retry delivery on a failed attempt. The endpoint must be active.

Manage your endpoints

  • Test events. Send one from Developer → Webhooks to check your
    receiver. It uses one of the endpoint's events, carries a synthetic payload
    with test: true and changes nothing.
  • Delivery attempts. Developer → Events shows each event, every delivery
    attempt and your server's answer.
  • Rotate the signing secret. The previous secret stays valid for 24 hours;
    install the new one on your server within that time.
  • Endpoint URL. It must use HTTPS on port 443, resolve only to public IP
    addresses, and contain no user name, password or fragment.

Webhooks