Configure and secure webhooks

Configure webhooks in the PSB: topics, HMAC SHA256 security and IP whitelisting.

Webhooks are the primary way to receive real-time notifications from the PSB. For every relevant event (an invoice received, a status change, a delivery confirmation) the PSB sends an HTTP POST request to your endpoint with the details of the event.

How do webhooks work in the PSB?

You register a webhook (a "hook") in the PSB with a URL and a topic. The PSB then sends all events for that topic to your URL. Each event contains the relevant data as a JSON payload.

Creating a webhook

Register a hook via the API:

POST /api/v1/hook

The main configuration elements:

FieldDescriptionurlThe HTTPS endpoint where the PSB sends events totopicThe type of event you want to listen for (e.g. InvoiceReceived)secretA secret key for HMAC signature verification

Note: avoid rapidly deleting and recreating hooks for the same partyId and topic. Due to race conditions in task processing, this can temporarily result in no active hook, causing events not to be delivered. Wait briefly after deleting a hook before creating a new one, or use an update instead of delete + create.

Commonly used topics
TopicWhenInvoiceReceivedA purchase invoice has been receivedInvoiceSentA sales invoice has been sent successfullyInvoiceSentErrorA sales invoice could not be deliveredInvoiceSentRetryA retry attempt has startedInvoiceResponseReceivedAn Invoice Response (status message) has been receivedMessageLevelStatusReceivedAn MLS status message has been received from the sending partyMessageLevelStatusSentAn MLS status message has been sent successfullyOrderReceivedA purchase order has been received
Message Level Status (MLS)

MLS (Message Level Status) is the successor to the older MLR and provides the sending party with feedback on the receipt and processing of a document. MLS is not enabled by default and must be configured per party via the reviews capability in the SMP configuration. Once MLS is enabled, the PSB handles it automatically: as the receiving Service Provider, the PSB sends an MLS message back to the sender after receipt and delivery.

When you send documents via the PSB yourself, you receive MLS feedback from the receiving party as a webhook event on the topic MessageLevelStatusReceived. The payload includes:

FieldDescriptiondocumentIdUnique ID of the MLS messagerefToDocumentIdID of the original documentdetails.statusCodePeppol status: AP (accepted), RE (rejected), AB (acknowledged)details.descriptionExplanation from the receiver

To receive MLS messages, the Peppol hook must contain the mlsType field. The possible values are ALWAYS_SEND (always send MLS back) and FAILURE_ONLY (only on rejections).

Tip: you can retrieve the full MLS document via GET /api/v1-beta/{partyId}/generic/{documentId}/download, but the webhook payload usually contains sufficient information.

Securing webhooks with HMAC

The PSB secures all webhook deliveries with HMAC SHA256 signatures. With every request the PSB sends the header:

X-EConnect-Signature: sha256={signature}
Implementing verification

To verify the signature:

  1. Take the raw JSON payload of the request.
  2. Calculate the HMAC SHA256 hash using your secret key (the same one you provided when creating the hook).
  3. Compare your calculated hash with the value in the X-EConnect-Signature header.
  4. If they match, the request is authentic.
Replay attack prevention

Also check the sentOn field in the payload. If this timestamp is older than 5 minutes, reject the request. This prevents replay attacks where an intercepted request is replayed at a later time.

Additional security options

In addition to HMAC verification, the PSB offers extra security layers:

  • IP whitelisting: restrict incoming requests to the PSB production IPs (104.40.188.59 and 104.47.148.207)
  • OAuth webhook authentication: the PSB can authenticate with your endpoint using OAuth2 credentials
  • Mutual SSL: use client certificates for mutual TLS authentication
Priority order

If you have multiple hooks configured, the PSB determines which hook is used based on:

  1. PartyId-level hooks take precedence over environment-level hooks
  2. Specific topics take precedence over wildcards
  3. At equal priority: hook-id as tiebreaker
HTTPS inbound hooks

Not every system can expose a webhook endpoint for incoming traffic. Think of on-premise ERP systems or secured networks without inbound internet connectivity. For these situations the PSB offers HTTPS inbound hooks.

Instead of the PSB sending an event to your URL, you reverse the flow: the PSB pushes documents to an internal endpoint using the provided credentials. You configure the action with the httpsin:// protocol:

httpsin://user:pass@inbound?token=$token$

The PSB uses the specified username and password to deliver the document. The $token$ placeholder is automatically replaced by the document token.

HTTPS inbound hooks are specifically designed for environments where the regular webhook API does not work because inbound internet traffic is not possible. In all other cases, standard webhooks are the recommended approach.

Hook filters

By default a hook triggers on every event for the configured topic. With filters you can refine this, so a hook only fires when the event meets a specific condition.

Filters use lambda expression syntax. You add the filter as a property on the hook configuration:

{
  "topic": "InvoiceReceived",
  "url": "https://jouw-endpoint.nl/webhook",
  "filter": "sender == \"0106:12345678\" && verdict.StartsWith(\"acc\")"
}

In this example the hook only triggers if the sender is 0106:12345678 and the verdict starts with acc (accepted).

Common use cases:

  • Only trigger on rejections (verdict.StartsWith("rej"))
  • Filter by a specific sender or recipient
  • Combine multiple conditions with && and ||

Filters are useful when you have multiple hooks on the same topic, but each system should only receive relevant events. This prevents unnecessary processing.

Wildcard topics

In addition to specific topics you can also use wildcard patterns to capture multiple event types with a single hook.

PatternMatchesSend*All send events (InvoiceSent, InvoiceSentError, InvoiceSentRetry, etc.)*ReceivedAll receive events (InvoiceReceived, OrderReceived, InvoiceResponseReceived, etc.)Invoice*All invoice-related events

Wildcards are particularly useful for catch-all hooks, for example a monitoring or logging endpoint that should receive all events. You can also combine wildcards with specific topics on other hooks. The priority order ensures that a more specific hook always takes precedence over a wildcard.

Best practices
  • Always use HTTPS for your webhook endpoint
  • Implement idempotent processing: in rare cases the PSB may deliver an event more than once
  • Return a 2xx status code quickly: the PSB treats any non-2xx response as an error and will retry the event
  • Log all received events for debugging and auditing
  • Use a queue on your side if processing takes time; confirm receipt first and process afterwards
Frequently asked questions
How do I verify an incoming webhook with HMAC SHA256?

Take the raw JSON body of the request, compute the HMAC SHA256 hash using the secret you specified when creating the hook and compare it with the value in the X-EConnect-Signature header (after the sha256= prefix). If they match, you know the request came from the PSB and was not modified in transit.

Why should I check the sentOn field in the payload?

Verify that sentOn is no older than 5 minutes. If the timestamp is too old, reject the request. This prevents replay attacks where a previously intercepted request is resubmitted later, even if the signature is technically valid.

How do I avoid problems with idempotent processing and retries?

Implement idempotent processing on your side, because the PSB may deliver an event more than once in rare cases. Also return an HTTP 2xx status quickly: any other response counts as an error and triggers a retry. For heavy processing, confirm receipt first and then process via a queue.


Prefer to retrieve documents in bulk? Check out the batch hook.

View the webhook endpoints