POST request that the API sends to your server when a document event occurs. Use webhooks to learn that a document was sent, received or failed without polling the API.
Each webhook belongs to one company. A sandbox company and a production company each have their own API key and their own webhooks.
Set up a webhook
1
Create the webhook
Send the URL of your receiver and the events that you want. See Create a webhook.
2
Store the signing secret
The response contains the
secret. Put it in a secret manager or an environment variable. See Get the signing secret.3
Implement the receiver
Verify the
X-Signature header, return a 2xx status, then process the event. See Implement the receiver.4
Send a test event
Call the test endpoint to deliver an event that you define. See Send a test event.
5
Check the result
Read the response of the test call and the delivery history.
Create a webhook
string
required
The address of your receiver. See Receiver URL requirements.
string[]
required
The event types that this webhook receives. See Events. An unknown value gives HTTP
422.boolean
default:"true"
A flag that the API stores and returns with the webhook.
201:
Get the signing secret
The API makes the secret when you create the webhook. The secret is a string of 64 hexadecimal characters. You cannot supply your own value.- The
secretfield is in the response of the create, get, list and update calls. You can read it again at any time. - You cannot change the secret of a webhook. To rotate the secret, create a new webhook, deploy the new secret to your receiver, then delete the old webhook.
- Keep the secret in a secret manager or an environment variable. Do not put it in source code, in logs or in client-side code.
Receiver URL requirements
- Method: the receiver accepts
POSTrequests with a JSON body. - Protocol:
httpandhttpsURLs are accepted. Usehttpsfor a production company. - Certificate: an
httpsreceiver must have a certificate from a trusted certificate authority. Delivery to a receiver with a self-signed certificate fails. - Redirects: the API does not follow redirects. A
3xxresponse is a failed delivery. - Basic authentication: you can put credentials in the URL, for example
https://username:password@example.com/webhook.
Event payload
The API sends each event as aPOST request with these headers:
The body is a JSON object:
The payload does not contain the document. Use
data.document_id to get it:
cURL
Events
Document events are sent to your webhooks. Thedata object of each one is {"document_id": "doc-..."}.
To get the document that a
document.received event announces, see Receive documents.
The events field of the create call also accepts the values below. They are reserved: do not build a receiver that depends on them.
document.error, email.received, email.received.failed, email.sent, email.sent.failed, peppol.received, peppol.received.failed, peppol.sent, peppol.sent.failed
The OpenAPI specification gives
events as a list of strings without an enumeration. This page is the reference for the permitted values.Verify the signature
Each request has anX-Signature header. Verify it before you process the event. A request without a correct signature did not come from e-invoice.be.
The API computes the signature as follows:
- It serialises the complete event (all fields, not only
data) to a canonical JSON string. - It encodes that string as UTF-8.
- It computes the HMAC-SHA256 of those bytes with the webhook secret as the key.
- It sends
sha256=followed by the digest in lowercase hexadecimal.
Canonical JSON string
The canonical string is the output of Pythonjson.dumps(payload, sort_keys=True):
- The keys of each object are in ascending order, at each level.
- The separator between items is a comma followed by one space (
,). The separator between a key and its value is a colon followed by one space (:). There are no line breaks and there is no other white space. - Each character outside the printable ASCII range is written as a
\uXXXXescape with lowercase hexadecimal digits. A character above U+FFFF is written as a surrogate pair. - The characters
"and\are escaped with a backslash. Line feed, carriage return, tab, backspace and form feed are written as\n,\r,\t,\band\f. The character/is not escaped.
Test vector
Use this vector to test your code before you connect it to the API. Secret:\n, \" and \u26a1\ufe0f are literal escape sequences. They are not line breaks or decoded characters.
Signature:
Implement the receiver
Each sample is a complete receiver. It reads the secret from the environment variableE_INVOICE_WEBHOOK_SECRET, parses the body, builds the canonical string, compares the signature in constant time and returns a status code.
The document events contain only strings and integers, and the samples give the correct canonical string for them. If you send a test event with decimal numbers in
data, the JavaScript serialiser can give a different result, because JavaScript does not keep the difference between 1.0 and 1.- Verify the signature before you use the payload.
- Compare the signatures with a constant-time function.
- Return a
2xxstatus in less than 10 seconds. Do slow work (get the document, update your database) in a background job. - Return
401for a wrong signature. Do not return a redirect.
Delivery and retries
- A delivery is successful when your receiver returns a
2xxstatus in less than 10 seconds. - Each other result is a failure: a status that is not
2xx(redirects included), a timeout, a connection error or a certificate error. - The API makes a maximum of 3 attempts for each event. The wait time between attempts increases. After the last failed attempt, the API does not send the event again.
- The wait time between attempts is not a fixed value. Do not build logic that depends on it.
Duplicates and order
- Duplicates: your receiver can get the same event more than once, for example when it processed the request but answered too late. Each attempt has a new
id, a newcreated_atand a new signature. Thus theidcannot identify a duplicate. De-duplicate on the combination oftypeanddata.document_id. - More than one webhook: if two webhooks subscribe to the same event type, each one gets a request with its own
id. - Order: the order of delivery is not guaranteed. A
document.sent.failedevent can arrive after a laterdocument.sentevent for the same document. Do not derive the state of a document from the order of the events. Get the document and read itsstate.
Test on your computer
1
Start your receiver
Run one of the receiver samples on a local port, for example port 3000.
2
Open a tunnel
The API cannot reach
localhost. Use a tunnel tool (for example ngrok or Cloudflare Tunnel) to get a public https address that forwards to your local port.3
Create a webhook in a sandbox company
Use the API key of a sandbox company and the public address of the tunnel as
url. Set E_INVOICE_WEBHOOK_SECRET to the secret from the response and start the receiver again.4
Send events
Send a test event with the test call below. To get a
document.received event with a document that you can get from the API, use Simulate inbound in the sandbox company. To get a document.sent event, send a document from the sandbox company. See Test mode and sandbox companies.Send a test event
The test call sends an event with theevent_type and data that you supply. The webhook must subscribe to the event type.
cURL
200. The response has one of two forms.
If the API delivered the event during the call, webhook_delivery_result contains the result of that one attempt:
200, with success set to false:
error field contains the status code and the first 100 characters of the response body of your receiver. An event_type that is unknown, or that the webhook does not subscribe to, gives HTTP 422.
The
document_id in a test event is the value that you supply. It is not necessarily a document that exists.Delivery history
The history call returns the recorded delivery results of one webhook.cURL
Limits of the history:
- There is no pagination and there are no filters. The call returns all recorded rows of the webhook.
- A row has no timestamp and no event
id. - The history has one row for each event: the successful attempt, or the last failed attempt. Failed attempts that were followed by a retry are not recorded.
- Not each delivery is recorded. An empty history is not proof that the API sent no events.
- An unknown webhook identifier gives an empty
historylist, not HTTP404.
Manage webhooks
Each call needs the API key of the company that owns the webhook. A wrong or absent key gives HTTP401. An unknown webhook identifier gives HTTP 404 on the get, update, delete and test calls. See Errors and troubleshooting.
List webhooks
cURL
Get a webhook
cURL
Update a webhook
The update call acceptsurl, events and enabled. All fields are optional; the API changes only the fields that you send. The secret does not change.
cURL
Delete a webhook
cURL
404. To stop all deliveries to a receiver, delete its webhook.
Security checklist
- Use an
httpsURL with a certificate from a trusted certificate authority. - Verify the
X-Signatureheader of each request, with a constant-time comparison. - Keep the secret out of source code and logs.
- Treat the webhook URL as readable by each holder of the API key. Credentials in the URL are returned in clear text by the get and list calls.
- Do not trust the payload for business decisions. Get the document from the API with
data.document_id. - To rotate the secret, create a new webhook and delete the old one.
Next Steps
Receive documents
Get the documents that a
document.received event announces.Document lifecycle and delivery tracking
Read the document states behind the sent and failed events.
Errors and troubleshooting
Find the cause of an error response from the API.
Test mode and sandbox companies
Test webhooks in a sandbox company with Simulate inbound.