Skip to content

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 POST requests with application/json body
  • Respond with a 2xx status 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.

  • subTenantId maps back to the externalSubTenantId you supplied when creating the customer.
  • legalEntityId is the getpeppr legal entity id for follow-up API calls.
  • status is one of verified, verification_failed, awaiting_authz, or active.
  • environment tells you whether the transition happened in sandbox or production.
For the full multi-tenant sending flow, including sender.externalSubTenantId and production send gates, see Platform Sending & Webhooks.
legal_entity.registered
{
  "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)

Inbound reception is a pilot feature. Contact support@getpeppr.dev to enable it for your account.

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 arrived
  • inbound.creditnote.received — a credit note addressed to your Legal Entity arrived
Not to be confused with 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).

inbound.invoice.received
{
  "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 verification

Payload Format

Every webhook request contains a JSON body with the following structure:

  • id — unique event ID (identifies one delivery; for inbound events deduplicate on data.receivedDocumentId instead)
  • type — event type string
  • data — event-specific payload
  • createdAt — ISO 8601 timestamp
Webhook Payload
{
  "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/sdk to verify the Getpeppr-Signature HMAC-SHA256 header
  • Deduplicate — use the stable resource key in data (for inbound events: data.receivedDocumentId); event.id only identifies a single delivery and does not cover redelivery repair
  • Respond quickly — process asynchronously and return 200 immediately
  • Handle retries — getpeppr retries failed deliveries with exponential backoff
Always verify the Getpeppr-Signature header in production using await webhooks.constructEvent() from @getpeppr/sdk. Without verification, an attacker could send fake events to your endpoint.