Webhooks
Receive real-time notifications when invoices move through delivery, or when platform sub-tenants change lifecycle state.
Setup
Configure your webhook endpoint in the getpeppr console under Settings → Webhooks. getpeppr will send a POST request to your endpoint for each event.
Requirements
- Your endpoint must accept
POSTrequests withapplication/jsonbody - Respond with a
2xxstatus code within 5 seconds - Use HTTPS in production
Event Types
| Event | Description |
|---|---|
invoice.sent | Invoice successfully delivered to recipient's access point |
invoice.accepted | Recipient accepted the invoice |
invoice.refused | Recipient rejected the invoice |
invoice.error | Delivery failed (final state) |
invoice.registered | Cleared by tax authority (e.g., KSA, PT) |
invoice.received | Receipt acknowledged by recipient |
invoice.paid | Payment confirmed by recipient |
legal_entity.registered | Platform sub-tenant reached a verified or active state |
legal_entity.verification_failed | Platform sub-tenant registry verification failed |
legal_entity.awaiting_authz | Platform sub-tenant authorisation email is awaiting customer action |
legal_entity.registration_failed | Platform sub-tenant identity verified but network (SMP) registration failed |
peppol_identifier.verified | A Peppol identifier completed registry verification |
peppol_identifier.verification_failed | A Peppol identifier failed registry verification |
test.ping | Test event sent during endpoint setup |
inbound.invoice.received | An invoice addressed to your Legal Entity was received from the Peppol network (pilot — contact support to enable) |
inbound.creditnote.received | A credit note addressed to your Legal Entity was received from the Peppol network (pilot — contact support to enable) |
* | Wildcard — subscribes to all event types |
Platform Lifecycle Webhooks
Platform accounts receive the legal_entity.* events above on the same endpoint
delivery channel as invoice webhooks: signed with Getpeppr-Signature, retried
automatically, and filterable with the same event subscriptions.
subTenantIdmaps back to theexternalSubTenantIdyou supplied when creating the customer.legalEntityIdis the getpeppr legal entity id for follow-up API calls.statusis one ofverified,verification_failed,awaiting_authz, oractive.environmenttells you whether the transition happened insandboxorproduction.
sender.externalSubTenantId
and production send gates, see
Platform Sending & Webhooks.
{
"id": "evt_1a2b3c",
"type": "legal_entity.registered",
"data": {
"subTenantId": "customer_8412",
"legalEntityId": "7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b",
"peppolId": "GB:CRN:12345678",
"status": "active",
"environment": "production",
"occurredAt": "2026-06-01T10:05:00.000Z"
},
"createdAt": "2026-06-01T10:05:00.000Z"
}Inbound Reception (Pilot)
When a supplier on the Peppol network sends an invoice or credit note to one of your Legal Entities, getpeppr stores the document and dispatches one of these events:
inbound.invoice.received— an invoice addressed to your Legal Entity arrivedinbound.creditnote.received— a credit note addressed to your Legal Entity arrived
invoice.received — that existing outbound event
means “a document you sent was acknowledged by the recipient’s access point”.
The inbound.* events mean a document was sent to you by a third party.
Document embed
The UBL XML content is embedded in the webhook payload as a base64-encoded string (field
data.document.content). Documents larger than 512 KB are stored but
not embedded: content will be null and
contentOmittedReason will be "size". A retrieval API is planned for
Phase 2.
Deduplication
Delivery is at-least-once. Deduplicate on data.receivedDocumentId — it is
the stable idempotency key for inbound events: one received document, one id,
however many times it is delivered. event.id only identifies a single delivery
attempt of one outbox row and does not dedupe duplicates created by internal
redelivery repair (the same document can arrive under different event.id values).
{
"id": "evt_def456ghi789",
"type": "inbound.invoice.received",
"data": {
"receivedDocumentId": "9f1a2b3c-4d5e-6f70-8a9b-0c1d2e3f4a5b",
"legalEntityId": "7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b",
"externalSubTenantId": "customer_8412",
"documentType": "invoice",
"sender": {
"peppolId": "0208:0123456789",
"name": "Supplier SA"
},
"invoiceNumber": "INV-123",
"receivedAt": "2026-06-11T08:00:00.000Z",
"providerDocumentId": "storecove-guid-abc123",
"document": {
"format": "ubl",
"encoding": "base64",
"content": "<base64 UBL XML>",
"contentOmittedReason": null,
"sizeBytes": 12345
}
},
"createdAt": "2026-06-11T08:00:01.000Z"
}Handler Example
Here's a complete webhook handler using the SDK types. The WebhookEvent
type ensures type safety for all event payloads.
import { webhooks } from "@getpeppr/sdk";
import type { WebhookEvent } from "@getpeppr/sdk";
// In your Express / Hono / Fastify route handler
app.post("/webhooks/peppol", async (req, res) => {
// 1. Verify the signature (critical for security)
let event: WebhookEvent;
try {
event = await webhooks.constructEvent(
req.body, // raw body string (NOT parsed JSON)
req.headers["getpeppr-signature"] as string,
"whsec_your_secret" // from dashboard
);
} catch (err) {
console.error("Signature verification failed:", err);
return res.status(400).send("Invalid signature");
}
// 2. Handle the event
switch (event.type) {
case "invoice.sent":
console.log(`Invoice ${event.data.invoiceId} delivered!`);
break;
case "invoice.accepted":
console.log(`Invoice ${event.data.invoiceId} accepted!`);
break;
case "invoice.refused":
console.error(`Invoice ${event.data.invoiceId} refused`);
break;
case "invoice.error":
console.error(`Invoice ${event.data.invoiceId} delivery failed`);
break;
case "invoice.paid":
console.log(`Invoice ${event.data.invoiceId} paid!`);
break;
}
// 3. Always respond quickly — process async if needed
res.status(200).send("ok");
});
// Event types:
// "invoice.sent" — delivered to recipient's access point
// "invoice.accepted" — recipient accepted the invoice
// "invoice.refused" — recipient rejected the invoice
// "invoice.error" — delivery failed (final state)
// "invoice.registered" — cleared by tax authority
// "invoice.received" — receipt acknowledged by recipient
// "invoice.paid" — payment confirmed by recipient
// "inbound.invoice.received" — an invoice addressed to your Legal Entity arrived
// "inbound.creditnote.received" — a credit note addressed to your Legal Entity arrived
// "legal_entity.registered" — sub-tenant reached verified/active
// "legal_entity.verification_failed" — sub-tenant registry check failed
// "legal_entity.awaiting_authz" — sub-tenant authorisation requested
// "test.ping" — test event for setup verificationPayload Format
Every webhook request contains a JSON body with the following structure:
id— unique event ID (identifies one delivery; for inbound events deduplicate ondata.receivedDocumentIdinstead)type— event type stringdata— event-specific payloadcreatedAt— ISO 8601 timestamp
{
"id": "evt_abc123def456",
"type": "invoice.sent",
"data": {
"invoiceId": "12345",
"invoiceNumber": "INV-2026-001",
"environment": "sandbox"
},
"createdAt": "2026-03-01T10:05:00.000Z"
}Security
Webhook requests include an Getpeppr-Signature header that you should
verify to ensure the request is from getpeppr and not a third party.
Best Practices
- Verify the signature — use
await webhooks.constructEvent()from@getpeppr/sdkto verify theGetpeppr-SignatureHMAC-SHA256 header - Deduplicate — use the stable resource key in
data(for inbound events:data.receivedDocumentId);event.idonly identifies a single delivery and does not cover redelivery repair - Respond quickly — process asynchronously and return
200immediately - Handle retries — getpeppr retries failed deliveries with exponential backoff
Getpeppr-Signature header in production using
await webhooks.constructEvent() from @getpeppr/sdk.
Without verification, an attacker could send fake events to your endpoint.