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

Geen UI beschikbaar: het is op dit moment niet mogelijk om een hook te configureren via de gebruikersinterface van het platform. Hooks worden ingesteld via de API of via eConnect support. Een hook-self-service-UI staat op de roadmap (gepland voor Q4 2026).

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": "3e3b336c-6563-431c-93bc-6b109dedfcc5@econnect.eu",
    "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.

Problemen oplossen

Hieronder staan veelvoorkomende problemen met webhooks en hoe je ze oplost.

SymptoomOorzaakOplossingWebhooks komen niet aanEndpoint niet bereikbaar (timeout 100 sec)HTTPS + geldig SSL-certificaat vereist; CSRF-bescherming uitschakelen op webhook-endpointWebhook afgewezen als onveiligX-EConnect-Signature validatie faaltHMAC SHA256-berekening controleren: payload × secret → hex-string met prefix sha256=Relay attack blokkadesentOn-veld ouder dan 5 minutenEndpoint verwerkt te langzaam of klok staat verkeerd"No such host is known" (DNS-fout)Hostnaam van het webhook-endpoint is niet (meer) resolvbaar, bijv. na hernoemen van de ERP-omgeving of verlopen DNS-recordDNS-record van het endpoint-domein controleren en herstellen; daarna gemiste events ophalen via de batch-endpointHookSentError (definitieve mislukking)PSB heeft 5 dagen geprobeerd, geen 2xx responseEndpoint logs checken; HookSentError-topic monitoren (zie hieronder); gemiste events ophalen via batch-endpointOude facturen opnieuw aangebodenAPI retourneert geen 2xx bij eerste ontvangstEndpoint moet altijd 2xx teruggeven, ook bij asynchrone verwerkingDubbele verwerking bij retryGeen idempotency-controleX-EConnect-Delivery-header (unieke UUID) gebruiken voor deduplicatie
HookSentError monitoren en herstellen

De PSB stuurt webhook-events (bijv. InvoiceReceived bij een ontvangen e-factuur) naar het geconfigureerde endpoint. Is dat endpoint onbereikbaar — door een timeout, een SSL-fout of een DNS-fout zoals No such host is known — dan retryt de PSB met exponential backoff gedurende 5 dagen (timeout 100 sec per poging). Per poging volgt een HookSentRetry-event; na 5 dagen zonder 2xx-response volgt HookSentError als eindstatus en stopt de PSB met proberen.

Gevolg: een binnengekomen e-factuur wordt niet bij de klant afgeleverd zolang het endpoint onbereikbaar is, zonder dat de klant dit direct merkt. Aanbevolen opvolging:

  • Monitoring inrichten: abonneer een hook (bijv. een mail-hook) op het topic HookSentError, zodat een definitieve afleverfout actief wordt gesignaleerd. Overweeg ook HookSentRetry voor vroege detectie.
  • Root cause verhelpen: bij een DNS-fout het DNS-record van het endpoint-domein controleren en herstellen; bij timeouts het endpoint sneller laten antwoorden (2xx binnen 100 sec, zware verwerking asynchroon).
  • Gemiste events herstellen: events die tijdens de onbereikbaarheid zijn mislukt, kunnen alsnog worden opgehaald via de batch-endpoint. Het 5-daagse retry-venster betekent dat tijdig herstel binnen die periode reguliere aflevering nog kan opvangen.
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