Skip to content

Sending & webhooks

Send invoices on behalf of your customers, and keep your UI in sync with lifecycle webhooks.

Sending as a customer

Send an invoice on a customer's behalf with the standard POST /v1/invoices endpoint — just add a sender that points to one of your customers. Identify them by their getpeppr legalEntityId or by your own externalSubTenantId. Put your own reference in externalSubTenantId: a value in legalEntityId that is not a getpeppr id is refused with 404 before anything is sent. Every refusal carries a sentence to show a human: in message, or in error when the body has no message (Platform errors).

  • Authoritative seller — the supplier identity (name, address, VAT, Peppol ID) is taken from the customer's verified legal entity. Identity fields in a payload from are accepted but stripped (no error returned), so you can't accidentally mis-state the sender. Only the seller contact is kept — from.contactName, from.phone and from.email — because a legal entity has no contact of its own. Domestic German invoices require all three, and are refused with 422 (DE-R-002) without them.
  • Send gate — outbound sending remains independent from receive discovery. Production requires a verified, attested identity published at Storecove; sandbox requires a verified or no_registry identity published at Storecove. The gate does not read networkDiscovery, so a customer can still show provisioning while outbound sends are accepted. If the outbound conditions are not met, the send is rejected with 422.
  • VAT identifier — invoices that carry VAT (including 0% and tax-exempt) require the sending customer to hold a registered VAT identifier. getpeppr registers it automatically when verification proves VAT registration; a 422 mentioning "no VAT identifier registered" means the entity needs a backfill — see Network identifiers and VAT.
  • Requires a master key with the legal_entities:send_as scope.
Single-tenant sending is unchanged: omit sender and a standard key behaves exactly as before. Send it anyway and both routes — POST /invoices and the raw-UBL route — refuse with 403, rather than dropping a security field in silence. Send-as activates only for a master key, and it works on both routes.

Don't have one? In the sandbox, an organisation admin starts the platform sandbox trial from the console overview and then creates a sandbox master key from the console. Production platform access is set up with our team — email hello@getpeppr.dev to request it.
import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_live_your_master_key" });

// Identify the customer by your externalSubTenantId…
await peppol.invoices.send({
  number: "INV-2026-001",
  sender: { externalSubTenantId: "customer_be_001" },
  to: {
    name: "Recipient SRL",
    peppolId: "0208:RECIPIENT_10_DIGIT_KBO",
    street: "Recipient street and number",
    city: "Brussels",
    postalCode: "1000",
    country: "BE",
  },
  lines: [{ description: "Consultation", quantity: 1, unitPrice: 90, vatRate: 0 }],
});

// …or by the getpeppr legalEntityId:
// sender: { legalEntityId: "7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b" }

Lifecycle webhooks

getpeppr pushes a webhook on each meaningful customer transition, so your platform receives actionable updates. Poll the Legal Entity endpoint when your UI needs intermediate states that are not emitted. The events ride the same delivery channel as your other webhooks — HMAC-signed, with automatic retries. See Webhooks for signature verification and retry behaviour.

Events

  • legal_entity.registered — public receive discovery verified the exact SML → SMP → Invoice metadata → active AS4 path. The event has status: "active", or "no_registry" for the sandbox registryless test scheme.
  • legal_entity.unsupported_scheme — no automatic validator is active for this scheme. No registry rejected the customer and no work remains in progress; contact support before sending.
  • legal_entity.verification_failed — registry mismatch or not found.
  • legal_entity.awaiting_authz — an attestation request is awaiting the customer.
  • legal_entity.awaiting_release — production only: the customer authorised, but another Peppol access point still holds the address. getpeppr registers it as soon as it is released; heldBy names that access point's SMP host when known. You then receive legal_entity.registered.
  • legal_entity.registration_failed — the customer's identity verified, but the network registration failed: the entity is not sendable. reason tells you why. In production it follows a definitive refusal, a 30-day wait for a release that never came (already_registered), or 30 days of provider errors (provider_error); retry with POST /v1/legal-entities/:id/attestation, no new confirmation needed.
Intermediate states (attested, provisioning) are not pushed as separate events. legal_entity.registered is emitted only after public cross-Access-Point discovery succeeds — never after identity verification or Storecove registration alone. Poll GET /v1/legal-entities/:id and read networkDiscovery for the fine-grained step. Outbound sending remains independent from this receive-only event.
Endpoints subscribed to * receive new lifecycle events automatically. An endpoint that explicitly selected only older event names must add the newer ones it needs: legal_entity.unsupported_scheme and legal_entity.awaiting_release.

Payload

Each event's data carries the fields below. peppolId is the customer's scheme and identifier joined by a colon (or null if not yet set).

  • subTenantId — your externalSubTenantId for this customer. You set this at creation, so it's present for sub-tenants you created via the API.
  • legalEntityId — the getpeppr legal entity id.
  • peppolId — the customer's Peppol identifier as "scheme:value", or null.
  • status — one of verified, no_registry, unsupported_scheme, verification_failed, registration_failed, awaiting_authz, awaiting_release, active.
  • environment — sandbox or production.
  • reason — on failures: name_mismatch or not_found (verification), already_registered, invalid_format or provider_error (registration). For legal_entity.registration_failed, this equals registrationDetail.reason on the current GET/LIST Legal Entity. The event payload itself is unchanged. On a not_found, GET /v1/legal-entities/:id may add verificationDetail.registryStatus, which tells you the registry knows the company but doesn't consider it active — when we have that finding to show.
  • heldBy — on legal_entity.awaiting_release only, when known: the SMP host of the access point that still holds the address.
  • occurredAt — ISO 8601 timestamp of the transition.
Map each event to your customer record by subTenantId — it's the same reference you supplied at creation, so no extra lookup is needed.
legal_entity.registered
{
  "id": "evt_1a2b3c",
  "type": "legal_entity.registered",
  "createdAt": "2026-06-01T10:05:00.000Z",
  "data": {
    "subTenantId": "customer_be_001",
    "legalEntityId": "7c9a1b34-2d5e-4f60-8a1b-9c2d3e4f5a6b",
    "peppolId": "0208:YOUR_10_DIGIT_KBO",
    "status": "active",
    "environment": "production",
    "occurredAt": "2026-06-01T10:05:00.000Z"
  }
}