Skip to main content
A webhook is an HTTP 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.
The API returns HTTP 201:
A company can have more than one webhook. Each webhook that subscribes to an event type receives its own request for that event.

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 secret field 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.
Each person or system that has the API key of the company can read the secret with the get and list calls. Protect the API key with the same care as the secret.

Receiver URL requirements

  • Method: the receiver accepts POST requests with a JSON body.
  • Protocol: http and https URLs are accepted. Use https for a production company.
  • Certificate: an https receiver 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 3xx response is a failed delivery.
  • Basic authentication: you can put credentials in the URL, for example https://username:password@example.com/webhook.
Register the final address of your receiver. If the address answers with a redirect (for example from http:// to https://, or from the apex domain to www), each delivery fails.
The get and list calls return the webhook URL in clear text, credentials included. If you use Basic authentication in the URL, use credentials that are only for this receiver.

Event payload

The API sends each event as a POST 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
For a failure event, get the document and read its state. To find the cause, see A document is in state FAILED and Document lifecycle and delivery tracking.

Events

Document events are sent to your webhooks. The data 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 an X-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:
  1. It serialises the complete event (all fields, not only data) to a canonical JSON string.
  2. It encodes that string as UTF-8.
  3. It computes the HMAC-SHA256 of those bytes with the webhook secret as the key.
  4. It sends sha256= followed by the digest in lowercase hexadecimal.

Canonical JSON string

The canonical string is the output of Python json.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 \uXXXX escape 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, \b and \f. The character / is not escaped.
Do not compute the HMAC of the raw request body. The body that the API sends is not the canonical string: it has no spaces after the separators, its keys are not sorted and non-ASCII characters are not escaped. Parse the body, build the canonical string, then compute the HMAC.
JSON.stringify in JavaScript, json_encode in PHP and JsonSerializer in C# do not give the canonical string. They do not sort the keys and they do not put a space after the separators. Use the serialiser functions in the samples below.

Test vector

Use this vector to test your code before you connect it to the API. Secret:
Event: the payload in Event payload. Canonical string (one line):
In this string, \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 variable E_INVOICE_WEBHOOK_SECRET, parses the body, builds the canonical string, compares the signature in constant time and returns a status code.
The Node.js sample uses only built-in modules. The Python sample uses Flask. The PHP sample is a script behind a web server. The C# sample is an ASP.NET Core minimal API.
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.
Rules for the receiver:
  • Verify the signature before you use the payload.
  • Compare the signatures with a constant-time function.
  • Return a 2xx status in less than 10 seconds. Do slow work (get the document, update your database) in a background job.
  • Return 401 for a wrong signature. Do not return a redirect.

Delivery and retries

  • A delivery is successful when your receiver returns a 2xx status 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.
If your receiver was unavailable for a longer time, get the missed documents with the inbox and outbox calls. See Receive documents.

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 new created_at and a new signature. Thus the id cannot identify a duplicate. De-duplicate on the combination of type and data.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.failed event can arrive after a later document.sent event for the same document. Do not derive the state of a document from the order of the events. Get the document and read its state.

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.
Before you open a tunnel, run your signature function on the test vector. If the result is not the given digest, the canonical string is wrong.

Send a test event

The test call sends an event with the event_type and data that you supply. The webhook must subscribe to the event type.
cURL
The API returns HTTP 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:
If the API put the event on the delivery queue, the response confirms only that:
A failed delivery during the call also gives HTTP 200, with success set to false:
The 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 history list, not HTTP 404.

Manage webhooks

Each call needs the API key of the company that owns the webhook. A wrong or absent key gives HTTP 401. 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 accepts url, events and enabled. All fields are optional; the API changes only the fields that you send. The secret does not change.
cURL
Send only the event types from the Events section. The update call does not check the values in events in the same way as the create call.

Delete a webhook

cURL
After the delete call, the webhook receives no new events and the get call gives HTTP 404. To stop all deliveries to a receiver, delete its webhook.

Security checklist

  • Use an https URL with a certificate from a trusted certificate authority.
  • Verify the X-Signature header 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.