Webhooks instellen en beveiligen

Webhooks configureren in de PSB: topics, HMAC SHA256-beveiliging en IP-whitelisting.

Webhooks zijn de primaire manier om real-time notificaties te ontvangen van de PSB. Bij elke relevante gebeurtenis, een ontvangen factuur, een statuswijziging, een afleverbevestiging, stuurt de PSB een HTTP POST-request naar jouw endpoint met de details van het event.

Hoe werken webhooks in de PSB?

Je registreert een webhook (een "hook") in de PSB met een URL en een topic. De PSB stuurt vervolgens alle events van dat topic naar jouw URL. Elk event bevat de relevante data als JSON-payload.

Een hook is gekoppeld aan een EndpointId, niet aan de partyId of legalEntityId van de organisatie. De partyId van de betrokken partij wordt in de payload van elke webhook meegegeven, zodat één hook events voor meerdere parties kan afhandelen. Per EndpointId kunnen meerdere hooks bestaan met verschillende topics.

Een webhook aanmaken

Registreer een hook via de API:

POST /api/v1/hook

De belangrijkste configuratie-onderdelen:

VeldBeschrijvingurlHet HTTPS-endpoint waar de PSB events naartoe stuurttopicHet type event waarop je wilt luisteren (bijv. InvoiceReceived)secureKeyHet gedeelde geheim waarmee de HMAC SHA256-handtekening van elke payload wordt berekend en gevalideerd

Let op: vermijd het snel achter elkaar verwijderen en opnieuw aanmaken van hooks voor hetzelfde partyId en topic. Door race conditions in de taakverwerking kan dit ertoe leiden dat er tijdelijk geen actieve hook is, waardoor events niet worden bezorgd. Wacht na het verwijderen van een hook even voordat je een nieuwe aanmaakt, of gebruik een update in plaats van delete + create.

Veelgebruikte topics
TopicWanneerInvoiceReceivedEen inkoopfactuur is ontvangenInvoiceSentEen verkoopfactuur is succesvol verzondenInvoiceSentErrorEen verkoopfactuur kon niet worden afgeleverdInvoiceSentRetryEen herzendpoging is gestartInvoiceResponseReceivedEen Invoice Response (statusberichten) is ontvangenMessageLevelStatusReceivedEen MLS-statusbericht is ontvangen van de verzendende partijMessageLevelStatusSentDe PSB heeft namens jou (als ontvangende Service Provider) een MLS verstuurd naar de verzendende SP. Bruikbaar voor monitoring van zelf-uitgegeven MLS-responsesOrderReceivedEen inkooporder is ontvangen
Message Level Status (MLS)

MLS (Message Level Status) is de opvolger van de oudere MLR en geeft de verzendende partij feedback over de ontvangst en verwerking van een document. MLS is niet standaard ingeschakeld en moet per party worden geconfigureerd via de reviews-capability in de SMP-configuratie. Zodra MLS is ingeschakeld, handelt de PSB het automatisch af: als ontvangende Service Provider stuurt de PSB een MLS-bericht terug naar de verzender na ontvangst en aflevering.

Wanneer je zelf documenten verstuurt via de PSB, ontvang je MLS-feedback van de ontvangende partij als webhook-event op het topic MessageLevelStatusReceived. De payload bevat onder andere:

VeldBeschrijvingdocumentIdUniek ID van het MLS-berichtrefToDocumentIdID van het oorspronkelijke documentdetails.statusCodePeppol-status: AP (accepted), RE (rejected), AB (acknowledged)details.descriptionToelichting van de ontvanger

Om MLS-berichten te ontvangen moet de Peppol-hook het veld mlsType bevatten. De mogelijke waarden zijn ALWAYS_SEND (altijd MLS terugsturen) en FAILURE_ONLY (alleen bij afwijzingen). De parameter werkt zowel op party-niveau (per Peppol-hook van een specifieke party) als op environment-niveau, waarmee je centraal MLS-gedrag stuurt voor alle parties in een omgeving zonder elke party-hook apart te configureren.

Zodra de PSB namens jou als ontvangende SP een MLS uitstuurt naar de verzendende SP, volgt een aparte webhook met topic MessageLevelStatusSent. De payload bevat het documentId van het MLS-bericht, het refToDocumentId van het oorspronkelijke document en de gebruikte statusCode. Gebruik dit topic om te monitoren of de zelf-uitgegeven MLS-responses goed worden afgeleverd.

MLS handmatig verzenden (testing)

Voor testdoeleinden kun je een MLS handmatig uitsturen via POST /api/v1-beta/generic/{documentId}/response met de gewenste status (AB, AP of RE) en een description. Bij een afwijzing (RE) is een lines-array beschikbaar waarin je per regel een statusReasonCode en aanvullende beschrijving meegeeft:

{
  "status": "RE",
  "description": "Document could not be delivered due to connectivity errors.",
  "lines": [
    {
      "statusReasonCode": "FD",
      "description": "Socket exception"
    }
  ]
}

Veelgebruikte waarden voor statusReasonCode zijn FD (failure of delivery, document permanent niet doorstuurbaar naar C4), SV (XML-schema-validatiefout), BV (business rule violation, fatale Schematron-fout) en BW (business rule warning, alleen samen met fatale fouten). Deze regels mappen op de Status Reason Codes in de UBL ApplicationResponse die via Peppol wordt verstuurd.

Tip: het volledige MLS-document kun je ophalen via GET /api/v1/{partyId}/generic/{documentId}/download, maar de webhook-payload bevat doorgaans voldoende informatie.

Webhook-payload

De payload bevat altijd ten minste de volgende velden:

VeldBeschrijvingtopicHet event-type, bijvoorbeeld InvoiceReceived of InvoiceSentErrordocumentIdServer-side identifier van het document, te gebruiken voor de download-endpointspartyIdIdentifier van de betrokken partij (gelijk aan de legalEntityId)createdByBron van het document: peppol, email, network, manualUpload, ksef of een andere kanaal-aanduiding. Gebruik dit veld om de downstream verwerking per bron te routerensentOnTijdstip van het event in UTC, ISO 8601

Geen factuurbedragen in de payload. De webhook-payload bevat bewust geen bedragen (totaal, btw, regelbedragen) of factuurregels. Bedragen zijn op te halen uit het XML-document zelf via de download-endpoints. Dit is een privacy-by-design keuze: notificatieverkeer mag geen financiële inhoud bevatten.

Inkomende documenten dedupliceer je op Invoice/ID + sender partyId + issueDate uit het opgehaalde XML-bestand. Dit is de verantwoordelijkheid van de ontvangende applicatie, niet van de PSB; de PSB garandeert at-least-once delivery van de notificatie zelf, maar bewaakt geen content-niveau dubbels in een ander e-facturatiekanaal.

Voorbeeld-payloads per topic

Elke payload bestaat uit de algemene velden hierboven, aangevuld met een topic-specifiek details-blok. De volgende voorbeelden zijn afkomstig uit de PSB DevOps-documentatie en bruikbaar als referentie bij het bouwen of testen van een webhook-consumer.

InvoiceReceived
{
  "topic": "InvoiceReceived",
  "partyId": "NL:KVK:12345678",
  "hookId": "1",
  "documentId": "5618b40a-822d-4e74-8894-b489617dbaa4",
  "message": "Invoice successfully received.",
  "details": {
    "id": "invoiceId",
    "sender": "0106:87654321",
    "createdBy": "peppol",
    "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
  },
  "createdOn": "2026-04-30T09:00:00.0000000+00:00",
  "sentOn": "2026-04-30T09:00:00.5000000+00:00"
}
InvoiceSent
{
  "topic": "InvoiceSent",
  "partyId": "NL:KVK:12345678",
  "hookId": "1",
  "documentId": "5618b40a-822d-4e74-8894-b489617dbaa4",
  "message": "Invoice successfully sent.",
  "details": {
    "id": "invoiceId",
    "recipient": "0106:87654321",
    "endpoint": "https://accp-ap.econnect.eu/as4/v1",
    "protocol": "As4",
    "attempt": "1",
    "messageId": "[email protected]",
    "sourceDocumentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
    "targetDocumentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
  },
  "createdOn": "2026-04-30T09:31:57.7444125+00:00",
  "sentOn": "2026-04-30T09:31:57.8703556+00:00"
}

InvoiceSentRetry en InvoiceSentError volgen dezelfde structuur. Bij Retry is attempt verhoogd; bij Error beschrijft het message-veld de uiteindelijke foutoorzaak.

MessageLevelStatusReceived
{
  "topic": "MessageLevelStatusReceived",
  "partyId": "NL:KVK:12345678",
  "hookId": "mls",
  "documentId": "615a914f-4a4d-4b3a-a258-828efb2817a6",
  "refToDocumentId": "d5e3bc34-96c8-4daf-b260-b7b182fa4760",
  "message": "'MessageLevelStatus' received. Status: 'Accept' Description: 'Document is accepted'.",
  "details": {
    "statusCode": "AP",
    "status": "Accept",
    "description": "Document is accepted",
    "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:peppol:edec:mls:1.0::2.1",
    "profileId": "urn:peppol:edec:mls"
  },
  "createdOn": "2026-03-11T22:43:18.2766651+00:00",
  "sentOn": "2026-03-11T22:43:18.8225397+00:00"
}

Interpreteer details.statusCode (AP accepted, RE rejected of AB acknowledged), details.description (toelichting van de ontvangende partij) en refToDocumentId (verwijzing naar het oorspronkelijke document waarvoor de status geldt).

MessageLevelStatusSent
{
  "topic": "MessageLevelStatusSent",
  "partyId": "NL:KVK:ECONNECTTS",
  "hookId": "mls",
  "documentId": "c4a31897-66ac-468c-a7e8-14a24f9af114",
  "refToDocumentId": "be7005ff-5555-41eb-afef-f5ffec2b29ed",
  "message": "'ApplicationResponse' successfully sent.",
  "details": {
    "statusCode": "AP",
    "status": "Accept",
    "description": "Document is accepted",
    "profileId": "urn:peppol:edec:mls",
    "sourceDocumentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:peppol:edec:mls:1.0::2.1",
    "attempt": "1",
    "protocol": "As4"
  },
  "createdOn": "2026-03-12T21:12:20.1621727+00:00",
  "sentOn": "2026-03-12T21:12:20.3726218+00:00"
}

Voor OrderSent en de overige order-topics gelden vergelijkbare structuren met een topic-specifiek details-blok. Bouw een parser die onbekende velden negeert: het details-element is dynamisch en kan in de toekomst nieuwe velden bevatten.

Conditional output topics

Een hook kan op v1-beta een output-array bevatten met regels die bepalen welk topic wordt gepubliceerd op basis van het resultaat van de actie. Daarmee kun je dynamische routering opzetten zonder dat de aanroepende code de fallback-logica zelf hoeft te kennen.

Elke output-regel bestaat uit twee velden:

VeldBeschrijvingwhenConditie, doorgaans een HTTP-statuscode zoals 400, 404 of 500topicTopic dat wordt gepubliceerd zodra de conditie matcht. De wildcard * (zoals in Send*Fallback) wordt geëxpandeerd naar het oorspronkelijke topic

De eerste matchende when bepaalt het resulterende topic. Als geen enkele regel matcht of als het output-veld ontbreekt, valt de hook terug op de standaard topics.

Een veelgebruikt scenario is Peppol met e-mailfallback: een Peppol-hook publiceert bij delivery-fouten (HTTP 400, 404 of 500) een Send*Fallback-topic, waarop een aparte mail-hook luistert die de factuur per e-mail uitstuurt.

{
  "id": "peppol",
  "action": "peppol",
  "name": "peppol hook",
  "topics": ["Send*"],
  "output": [
    { "when": "400", "topic": "Send*Fallback" },
    { "when": "404", "topic": "Send*Fallback" },
    { "when": "500", "topic": "Send*Fallback" }
  ],
  "isActive": true
}

Wanneer dezelfde mail-hook ook expliciet luistert op Send* (naast Send*Fallback), verschijnt deze automatisch als kanaal peppol-fallback in de queryRecipientParty-multi-channel-lookup. Daarmee kan de aanroepende code dit kanaal expliciet kiezen via ?channel=peppol-fallback op de send-endpoints.

Conditional output topics zijn niet beperkt tot e-mailfallback. Het mechanisme werkt voor elke combinatie van topics en kan bijvoorbeeld ook worden gebruikt om bij specifieke foutcodes naar een SFTP-hook of een externe webhook te routeren.

Webhooks beveiligen met HMAC

De PSB beveiligt alle webhook-bezorgingen met HMAC SHA256-handtekeningen. Bij elk request stuurt de PSB de header:

X-EConnect-Signature: sha256={handtekening}

De secureKey uit de hook-configuratie is geen aparte beveiligingslaag bovenop de signature, maar het gedeelde geheim waarmee de signature wordt gemaakt en gevalideerd. Wie de signature succesvol valideert, weet zeker dat de payload van de PSB komt en onderweg niet is aangepast. Daarnaast bevat de header X-EConnect-Delivery een unieke UUID per bezorging, bruikbaar als idempotency-sleutel aan de ontvangerkant.

Verificatie implementeren

Om de handtekening te verifiëren:

  1. Neem de ruwe JSON-payload van het request.
  2. Bereken de HMAC SHA256-hash met de secureKey die je bij het aanmaken van de hook hebt opgegeven.
  3. Vergelijk je berekende hash met de waarde in de X-EConnect-Signature-header (na het prefix sha256=).
  4. Als ze overeenkomen, is het request authentiek.
Replay-attack preventie

Controleer ook het sentOn-veld in de payload. Als dit tijdstip ouder is dan 5 minuten, wijs het request dan af. Dit voorkomt replay-attacks waarbij een onderschept request later opnieuw wordt afgespeeld.

Aanvullende beveiligingsopties

Naast HMAC-verificatie biedt de PSB extra beveiligingslagen:

  • IP-whitelisting: beperk inkomende requests tot de PSB-productie-IP's (104.40.188.59 en 104.47.148.207)
  • OAuth webhook-authenticatie: de PSB kan zich authenticeren bij jouw endpoint met OAuth2-credentials
  • Mutual SSL: gebruik client-certificaten voor wederzijdse TLS-authenticatie
Prioriteitsvolgorde

Als je meerdere hooks hebt geconfigureerd, bepaalt de PSB welke hook wordt gebruikt op basis van:

  1. PartyId-level hooks gaan voor environment-level hooks
  2. Specifieke topics gaan voor wildcards
  3. Bij gelijke prioriteit: hook-id als tiebreaker
HTTPS inbound hooks

Niet elk systeem kan een webhook-endpoint openstellen voor inkomend verkeer. Denk aan on-premise ERP-systemen of beveiligde netwerken zonder inbound internetverbinding. Voor deze situaties biedt de PSB HTTPS inbound hooks.

In plaats van dat de PSB een event naar jouw URL stuurt, draai je het om: de PSB pusht documenten naar een intern endpoint via de meegeleverde credentials. De actie configureer je met het httpsin://-protocol:

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

De PSB gebruikt de opgegeven gebruikersnaam en wachtwoord om het document af te leveren. De $token$-placeholder wordt automatisch vervangen door het documenttoken.

HTTPS inbound hooks zijn specifiek bedoeld voor omgevingen waar de reguliere webhook-API niet werkt omdat er geen inbound internetverkeer mogelijk is. In alle andere gevallen zijn standaard webhooks de aanbevolen aanpak.

Hook filters

Standaard triggert een hook bij elk event van het geconfigureerde topic. Met filters kun je dit verfijnen, zodat een hook alleen afgaat wanneer het event aan een bepaalde voorwaarde voldoet.

Filters gebruiken lambda-expressie syntax. Je voegt het filter toe als property op de hook-configuratie:

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

In dit voorbeeld triggert de hook alleen als de afzender 0106:12345678 is én het verdict begint met acc (accepted).

Veelvoorkomende toepassingen:

  • Alleen triggeren bij afwijzingen (verdict.StartsWith("rej"))
  • Filteren op een specifieke afzender of ontvanger
  • Combineren van meerdere voorwaarden met && en ||

Filters zijn handig als je meerdere hooks op hetzelfde topic hebt, maar elk systeem alleen relevante events moet ontvangen. Zo voorkom je onnodige verwerking.

Wildcard topics

Naast specifieke topics kun je ook wildcardpatronen gebruiken om meerdere eventtypes met één hook af te vangen.

PatroonMatchtSend*Alle verzend-events (InvoiceSent, InvoiceSentError, InvoiceSentRetry, etc.)*ReceivedAlle ontvangst-events (InvoiceReceived, OrderReceived, InvoiceResponseReceived, etc.)Invoice*Alle factuur-gerelateerde events

Wildcards zijn bijzonder handig voor catch-all hooks, bijvoorbeeld een monitoring- of logging-endpoint dat alle events moet ontvangen. Je kunt wildcards ook combineren met specifieke topics op andere hooks. De prioriteitsvolgorde zorgt ervoor dat een specifiekere hook altijd voorrang krijgt boven een wildcard.

Best practices
  • Gebruik altijd HTTPS voor je webhook-endpoint
  • Implementeer idempotent verwerking: de PSB kan een event in zeldzame gevallen meerdere keren bezorgen
  • Retourneer snel een 2xx statuscode: de PSB beschouwt elk niet-2xx-antwoord als een fout en zal het event opnieuw proberen
  • Log alle ontvangen events voor debugging en auditing
  • Gebruik een wachtrijsysteem (queue) aan jouw kant als de verwerking tijd kost, bevestig de ontvangst eerst en verwerk daarna
Veelgestelde vragen
Hoe verifieer ik een inkomende webhook met HMAC SHA256?

Neem de ruwe JSON-body van het request, bereken de HMAC SHA256-hash met de secret die je bij het aanmaken van de hook hebt opgegeven en vergelijk die met de waarde in de header X-EConnect-Signature (na het prefix sha256=). Als ze overeenkomen, weet je dat het request van de PSB komt en niet is gewijzigd onderweg.

Waarom moet ik het veld sentOn in de payload controleren?

Controleer of sentOn niet ouder is dan 5 minuten. Is het tijdstip te oud, wijs het request dan af. Zo voorkom je replay-aanvallen waarbij een eerder onderschept request later opnieuw wordt ingediend, ook als de handtekening technisch klopt.

Hoe voorkom ik problemen bij idempotente verwerking en retries?

Implementeer idempotente verwerking aan jouw kant, omdat de PSB een event in zeldzame gevallen meerdere keren kan bezorgen. Retourneer bovendien snel een HTTP 2xx-status: elk ander antwoord telt als fout en leidt tot opnieuw proberen; voor zware verwerking kun je eerst bevestigen en daarna via een queue verder werken.


Wil je documenten liever in bulk ophalen? Bekijk de batch hook.

Bekijk de webhook-endpoints

Gerelateerd