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
| Event | Wafeq sends it when |
|---|---|
nbr.<document_type>.cleared | NBR clears a standard document. |
nbr.<document_type>.reported | NBR accepts the report of a simplified document. |
nbr.<document_type>.error | The document has validation errors: Wafeq's checks stopped it before NBR, or NBR rejected it. |
nbr.<document_type>.failed | NBR does not accept the document for another reason, for example because it refuses the request. |
nbr.<document_type>.pending | A send to NBR gets no answer. Retry the document to settle it. |
nbr.device.activated | A device receives its production certificate, after registration, activation or renewal. It can sign. |
nbr.device.revoked | A 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:
| Field | Value |
|---|---|
event_id | The ID of the event (evt_…). It is the same value as the webhook-id header. |
event_type | The name of the event, for example nbr.invoice.cleared. |
payload | For 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. |
timestamp | The time when Wafeq created the event. |
test | Present 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.
| Step | Action |
|---|---|
| 1 | Check the signature over the raw body. |
| 2 | Find duplicates with the webhook-id header. |
| 3 | Store the event, then answer with a 2xx status. |
| 4 | Order 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):
| Attempt | Time after the previous attempt | Time after the first attempt |
|---|---|---|
| Retry 1 | 30 seconds | 30 seconds |
| Retry 2 | 1 minute | 1.5 minutes |
| Retry 3 | 2 minutes | 3.5 minutes |
| Retry 4 | 4 minutes | 7.5 minutes |
| Retry 5 | 8 minutes | 15.5 minutes |
| Retry 6 | 16 minutes | 31.5 minutes |
| Retry 7 | 32 minutes | 1 hour 4 minutes |
| Retry 8 | 1 hour 4 minutes | 2 hours 8 minutes |
| Retry 9 | 2 hours 8 minutes | 4 hours 16 minutes |
| Retry 10 | 4 hours 16 minutes | 8 hours 32 minutes |
- A retry has the same body and
webhook-idas the first attempt, and new
webhook-timestampandwebhook-signatureheaders. - 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
withtest: trueand 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.
Updated about 2 hours ago