1. 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.

EventWafeq ASP sends it when
taxpayer.createdYour organization gets its first active grant from a business.
taxpayer.updatedThe details or the status of a taxpayer change, or your grants from it change.
taxpayer.deletedYour organization loses its last active grant from a taxpayer.
document.sentThe delivery of a document to the provider of the buyer ends.
document.statusThe delivery of a document changes after that.
document.reportedThe FTA answers a report.
document.receivedA 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.created comes when the business approves your app. It comes also if your callback does not receive the authorization code.
  • taxpayer.updated comes when the taxpayer starts to deregister in EmaraTax, or when its registered details change. It also comes when your grants from it change.
  • taxpayer.deleted comes 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:

FieldValue
taxpayer_idThe Taxpayer ID (txpay_…).
tinThe TIN of the taxpayer.
participant_idThe Peppol ID of the taxpayer: 0235: followed by the TIN.
nameThe name of the taxpayer.
statusThe status of the taxpayer, for example onboarded, deregistration_pending or disconnected.
grantsYour active grants from the taxpayer: grant_id, label, scopes and metadata of each grant.
profileThe registered details: legal_name, trade_name, vat_number, legal_registration and address.
deletedtrue. Only taxpayer.deleted has this field.
  • taxpayer.updated also has previous_attributes, next to data. It contains only the fields that changed, with their old values.
  • taxpayer.deleted contains the last data before the change.
  • After each taxpayer event, read GET /grants to get the current state. Events can arrive late. Do not restore a connection because of a late taxpayer.created event.

document.sent

Why 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.

FieldValue
document_idThe document ID (inv_…).
source_idThe value that your app sent in source_id.
invoice_numberThe number of the document.
invoice_type_codeThe document type.
statusdelivered, failed or attention_required.
recipient_idThe address of the delivery: the Peppol ID of the buyer, or an FTA placeholder address.
seller_id, buyer_idThe Peppol IDs of the seller and the buyer.
currency, totalThe currency and the total amount of the document.

document.status

Why 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.

FieldValue
document_idThe document ID. Some events do not have it. Refer to the note below the table.
source_idThe value that your app sent in source_id.
invoice_numberThe number of the document.
delivery_statusThe new status of the delivery. For the values, refer to Track a document.
response_codeThe answer of the provider of the buyer: AP (accepted), AB (acknowledged) or RE (rejected). Other events have null.
reason_codeThe reason for the change. In a delivery receipt, it is the reason of the provider, for example BV, SV or FD.
message_idThe ID of the message that carried the document. The response of the submission gives the same value.
delivery_job_idThe ID of a queued delivery.
replacement_document_idThe 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. Use source_id to find the document. If your app sent no source_id, use message_id.
  • Wafeq ASP also sends document.status when it accepts a queued document, and when the queued delivery changes.

document.reported

Why 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.

FieldValue
document_idThe document ID.
source_idThe value that your app sent in source_id.
tax_authority_statusREPORTED (the FTA accepted the report), NOT_COMPLIANT (the FTA rejected the report) or WITHDRAWN.
submission_idThe ID of the report (tdd_…).
document_type_codeThe type of report: S, R or W. Refer to the list below the table.
supersedes_idFor an R report: the submission_id of the report that it replaces.
fta_statusThe answer of the FTA: accepted or rejected.
reason_code, detailThe 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 has tax_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

Why 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.

FieldValue
document_idThe document ID of the received document.
invoice_numberThe number of the document.
invoice_type_codeThe document type.
seller_id, buyer_idThe Peppol IDs of the seller and the buyer.
currency, totalThe currency and the total amount of the document.
statusreceived.

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:read scope. Without this scope, your organization gets no document events, also for the documents that your app submits.
  • Received documents also need the inbound:read scope. This applies to document.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_by identifies 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 /invoices and 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:

FieldValue
idThe ID of the event (evt_…). It is the same value as the webhook-id header.
typeThe name of the event, for example document.reported.
versionThe version of the format: "1".
created_atThe time when Wafeq ASP created the event.
participant_idThe Peppol ID of the taxpayer. In a test event, it is null.
environmentThe mode: live or sandbox.
testtrue for a test event, for example an event that you send from the portal. Otherwise false.
dataThe 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.

StepAction
1Check the signature over the raw body.
2Find duplicates with the webhook-id header.
3Store the event, then answer with a 2xx status.
4Put 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

Read 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

Store 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

Use 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: true and changes no document.

Webhooks in Sandbox

  • Wafeq ASP does not report documents to the FTA. Thus no real document.reported event comes.
  • Wafeq ASP simulates the delivery. The document.status events of a simulated delivery contain simulated: 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 the document.reported event. The test event then contains REPORTED, NOT_COMPLIANT or FAILED. FAILED occurs only in these test events.

For more about Sandbox, refer to Sandbox.


Webhooks