- Reference
Webhooks
This page uses these terms:
- Your organization: the account of your company in the Wafeq ASP portal. Your webhook endpoints belong to your organization.
- Mode: Live or Sandbox. Each mode has its own webhook endpoints, signing secrets and events.
- Grant: the permission that a business gives to your organization. A grant has an ID (
tgr_…) and scopes. The scopes are the permissions that the business approved. - Taxpayer: a business from which your organization has an active grant. Its ID is the Taxpayer ID (
txpay_…). - Document: an invoice or a credit note. Its ID is the document ID (
inv_…). - Delivery: the transfer of a document to the e-invoicing provider of the buyer, on Peppol.
- Report: the copy of a document that Wafeq ASP sends to the FTA, the UAE Federal Tax Authority. One document can have more than one report.
- Webhook endpoint: a URL on your server. Wafeq ASP sends events to this URL.
- Event: a message that Wafeq ASP sends to your webhook endpoint when something changes.
- Signing secret: the secret of one webhook endpoint. Your server uses it to check that an event comes from Wafeq ASP.
Use this page when you build the webhook receiver of your app. To add a webhook endpoint, refer to Set up your organization. To use the document events, refer to Track a document.
Example: one invoice, three events. Your app submits an invoice to a buyer on Peppol. Wafeq ASP delivers it to the provider of the buyer and sends document.sent. Some minutes later, the provider of the buyer accepts the invoice. Wafeq ASP sends document.status. Later, the FTA accepts the report. Wafeq ASP sends document.reported. Each event gives your app a result that it cannot get at the time of the submission.
The events
Wafeq ASP sends seven events. You select the events for each webhook endpoint.
| Event | Wafeq ASP sends it when |
|---|---|
taxpayer.created | Your organization gets its first active grant from a business. |
taxpayer.updated | The details or the status of a taxpayer change, or your grants from it change. |
taxpayer.deleted | Your organization loses its last active grant from a taxpayer. |
document.sent | The delivery of a document to the provider of the buyer ends. |
document.status | The delivery of a document changes after that. |
document.reported | The FTA answers a report. |
document.received | A document from another business arrives for a taxpayer. |
Taxpayer events
Why they matter. These events tell your app which businesses it can submit documents for. They also tell your app when the registered details of a taxpayer change. Wafeq ASP takes the seller details of each document from these registered details.
taxpayer.createdcomes when the business approves your app. It comes also if your callback does not receive the authorization code.taxpayer.updatedcomes when the taxpayer starts to deregister in EmaraTax, or when its registered details change. It also comes when your grants from it change.taxpayer.deletedcomes when the business revokes your last grant, or when your app revokes it. After this event, your app cannot call the API for this taxpayer.
The data of the three events contains these fields:
| Field | Value |
|---|---|
taxpayer_id | The Taxpayer ID (txpay_…). |
tin | The TIN of the taxpayer. |
participant_id | The Peppol ID of the taxpayer: 0235: followed by the TIN. |
name | The name of the taxpayer. |
status | The status of the taxpayer, for example onboarded, deregistration_pending or disconnected. |
grants | Your active grants from the taxpayer: grant_id, label, scopes and metadata of each grant. |
profile | The registered details: legal_name, trade_name, vat_number, legal_registration and address. |
deleted | true. Only taxpayer.deleted has this field. |
taxpayer.updatedalso hasprevious_attributes, next todata. It contains only the fields that changed, with their old values.taxpayer.deletedcontains the last data before the change.- After each taxpayer event, read
GET /grantsto get the current state. Events can arrive late. Do not restore a connection because of a latetaxpayer.createdevent.
document.sent
document.sentWhy it matters. This event tells your app if the document reached the provider of the buyer. It does not tell you if the buyer accepted the document. That answer comes later, in document.status.
| Field | Value |
|---|---|
document_id | The document ID (inv_…). |
source_id | The value that your app sent in source_id. |
invoice_number | The number of the document. |
invoice_type_code | The document type. |
status | delivered, failed or attention_required. |
recipient_id | The address of the delivery: the Peppol ID of the buyer, or an FTA placeholder address. |
seller_id, buyer_id | The Peppol IDs of the seller and the buyer. |
currency, total | The currency and the total amount of the document. |
document.status
document.statusWhy it matters. The provider of the buyer can reject a document after the delivery. A rejection can make it necessary to submit a corrected document. This event also shows the progress of a queued delivery.
| Field | Value |
|---|---|
document_id | The document ID. Some events do not have it. Refer to the note below the table. |
source_id | The value that your app sent in source_id. |
invoice_number | The number of the document. |
delivery_status | The new status of the delivery. For the values, refer to Track a document. |
response_code | The answer of the provider of the buyer: AP (accepted), AB (acknowledged) or RE (rejected). Other events have null. |
reason_code | The reason for the change. In a delivery receipt, it is the reason of the provider, for example BV, SV or FD. |
message_id | The ID of the message that carried the document. The response of the submission gives the same value. |
delivery_job_id | The ID of a queued delivery. |
replacement_document_id | The document ID of a corrected document that replaces this document. |
- An event that comes from the provider of the buyer (a delivery receipt) has no
document_id. Usesource_idto find the document. If your app sent nosource_id, usemessage_id. - Wafeq ASP also sends
document.statuswhen it accepts a queued document, and when the queued delivery changes.
document.reported
document.reportedWhy it matters. Wafeq ASP reports each document to the FTA. This event gives the answer of the FTA. Show this status to your users. It tells them if the FTA accepted the report of the document.
| Field | Value |
|---|---|
document_id | The document ID. |
source_id | The value that your app sent in source_id. |
tax_authority_status | REPORTED (the FTA accepted the report), NOT_COMPLIANT (the FTA rejected the report) or WITHDRAWN. |
submission_id | The ID of the report (tdd_…). |
document_type_code | The type of report: S, R or W. Refer to the list below the table. |
supersedes_id | For an R report: the submission_id of the report that it replaces. |
fta_status | The answer of the FTA: accepted or rejected. |
reason_code, detail | The reason that the FTA gives, if it gives one. |
The types of report:
S: the first report of the document.R: a corrected report, after the FTA rejected a report. Wafeq ASP does not deliver the document again.W: the withdrawal of a report. Wafeq ASP withdraws a report when the provider of the buyer rejects the document. The event then hastax_authority_status: WITHDRAWN.
If the provider of the buyer rejected the document with BV or SV, your app must submit a corrected document. Refer to Correct or cancel a document.
A document can have more than one report. Keep each event with the report that its submission_id identifies. GET /invoices/{document_id}/reports gives all the reports of a document. Old events can have no submission_id, document_type_code or supersedes_id.
document.received
document.receivedWhy it matters. This event tells your app that another business sent a document to a taxpayer, for example the invoice of a supplier. Your app can then get the document and show it to the user.
| Field | Value |
|---|---|
document_id | The document ID of the received document. |
invoice_number | The number of the document. |
invoice_type_code | The document type. |
seller_id, buyer_id | The Peppol IDs of the seller and the buyer. |
currency, total | The currency and the total amount of the document. |
status | received. |
To get the document, send GET /invoices/{document_id}/xml.
Which events your organization receives
These rules decide which events Wafeq ASP sends to your organization:
- Taxpayer events go to each organization that has an active grant from the taxpayer.
- Document events go to your organization only if one of your active grants from the taxpayer has the
invoices:readscope. Without this scope, your organization gets no document events, also for the documents that your app submits. - Received documents also need the
inbound:readscope. This applies todocument.received, and to the other events about a received document. - Documents of other apps. A taxpayer can approve more than one app. With
invoices:read, your organization receives the events of all the documents of the taxpayer.data.issued_byidentifies the organization that submitted the document. - Revoked grants. When your last grant from a taxpayer stops, document events stop immediately. This includes events that Wafeq ASP did not send yet.
- No past events. A new grant or a new webhook endpoint does not get the events of the past. To get earlier documents, use
GET /invoicesand the other list endpoints.
In Live, your organization can pay for received documents with document credits. A document credit is a prepaid unit of your organization. If your organization has no free credit, Wafeq ASP holds the document.received event. Wafeq ASP sends it when your organization gets more credits.
The format of an event
Wafeq ASP sends each event as a POST request with a JSON body. The body has these fields:
| Field | Value |
|---|---|
id | The ID of the event (evt_…). It is the same value as the webhook-id header. |
type | The name of the event, for example document.reported. |
version | The version of the format: "1". |
created_at | The time when Wafeq ASP created the event. |
participant_id | The Peppol ID of the taxpayer. In a test event, it is null. |
environment | The mode: live or sandbox. |
test | true for a test event, for example an event that you send from the portal. Otherwise false. |
data | The data of the event. |
In each document event, data also contains these fields:
taxpayer_id: the Taxpayer ID.grant_ids: the IDs of your grants that give your organization this event.issued_by: the ID of the organization that submitted the document. A received document does not have this field.
Example of a document.reported event. The example shows only some of the fields in data:
{
"id": "evt_6f1c2b7d0e9a4c3b8f5d2a1e0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b",
"type": "document.reported",
"version": "1",
"created_at": "2026-10-15T09:12:44.512031+00:00",
"participant_id": "0235:1999200000",
"environment": "live",
"test": false,
"data": {
"document_id": "inv_0123456789ABCDEFGHIJ",
"source_id": "erp-invoice-8812",
"tax_authority_status": "REPORTED",
"fta_status": "accepted",
"submission_id": "tdd_A1b2C3d4E5f6G7h8J9k0",
"document_type_code": "S",
"supersedes_id": null,
"taxpayer_id": "txpay_K1m2N3p4Q5r6S7t8U9v0",
"grant_ids": ["tgr_W1x2Y3z4A5b6C7d8E9f0"]
}
}Process an event: the four steps
Wafeq ASP sends each event at least one time. Thus your server can receive the same event more than one time. 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 | Put the events in order with created_at. |
Step 1: Check the signature over the raw body
Each request has three headers, in the Standard Webhooks format:
webhook-id: the ID of the event.webhook-timestamp: the time of the delivery attempt.webhook-signature: the signature of the ID, the timestamp and the body.
Check the signature with a Standard Webhooks library and the signing secret of the endpoint. Use the raw bytes of the body. Do not parse the JSON and serialize it again before the check, because this changes the bytes.
from standardwebhooks.webhooks import Webhook
event = Webhook(signing_secret).verify(raw_body, request_headers)The library also checks the timestamp. Keep this check, because it stops old requests that someone sends again. If the check fails, do not process the request.
Step 2: Find duplicates with the webhook-id header
webhook-id headerRead the webhook-id header. If your app already stored this ID, answer with a 2xx status and do nothing more.
Step 3: Store the event, then answer with a 2xx status
2xx statusStore the event in your database. Then answer with a 2xx status. Process the event after you answer.
If your server does not answer with a 2xx status, Wafeq ASP sends the event again later. The time between the attempts increases after each attempt.
Step 4: Put the events in order with created_at
created_atUse created_at to put the events of one document or one taxpayer in the correct order. Do not let an older event replace the data of a newer event.
After the receiver works
These items are not steps. They tell you how to keep the receiver in good condition.
- Rotate the signing secret. You can rotate the signing secret of an endpoint in the Wafeq ASP portal. The old secret stays valid for 24 hours. Install the new secret on your server in this period.
- See the delivery attempts. The Wafeq ASP portal shows each delivery attempt and the answer of your server. You can send a failed event again from the portal.
- Rules for the endpoint URL. The URL must use HTTPS on port 443. Its host name must resolve only to public IP addresses. The URL must not contain a user name, a password or a fragment.
- Live and Sandbox. Add a webhook endpoint in each mode. Each endpoint has its own signing secret.
- Test events. To test your receiver, send a test event from Developer → Webhooks in the Wafeq ASP portal. The endpoint must be active. A test event uses one of the events of the endpoint. It contains
test: trueand changes no document.
Webhooks in Sandbox
- Wafeq ASP does not report documents to the FTA. Thus no real
document.reportedevent comes. - Wafeq ASP simulates the delivery. The
document.statusevents of a simulated delivery containsimulated: true. - When the buyer is a Sandbox taxpayer, the organizations of the buyer receive
document.received. - To test
document.reported, select an FTA answer for the test event. The endpoint must have thedocument.reportedevent. The test event then containsREPORTED,NOT_COMPLIANTorFAILED.FAILEDoccurs only in these test events.
For more about Sandbox, refer to Sandbox.
Updated about 14 hours ago