Integrating Peppol e-Invoicing into SaaS: Infrastructure vs. Custom Build
These articles are AI-generated summaries. Please check the original sources for full details.
How to add Peppol e-invoicing to your SaaS without making it your team’s problem
The Peppol network provides a standardized framework for B2B electronic invoicing across Europe. Belgium has already required structured domestic B2B e-invoicing since January 1, 2026.
Why This Matters
Building a custom Peppol integration creates a long-term maintenance tax due to the complexity of UBL 2.1 (Peppol BIS Billing 3.0) and country-specific validation rules. Rather than managing XML generation, Access Point connectivity, and directory lookups manually, engineers should treat Peppol as infrastructure—similar to a payment processor—to avoid compliance drift and diversion from core product development.
Key Insights
- EU Compliance Timelines: Belgium mandated B2B e-invoicing on Jan 1, 2026; Germany required receiving capabilities on Jan 1, 2025; France begins rollout Sept 1, 2026.
- SaaS Architecture Shapes: A ‘Verified Sender’ model (one legal entity) is simple, whereas a ‘Delegated Sender’ model (SaaS sending on behalf of customers) requires complex KYB and authorization gates.
- UBL Complexity: The standard involves UBL 2.1 conforming to Peppol BIS Billing 3.0 with hundreds of optional fields and non-trivial validation trees.
- @getpeppr SDK/CLI: Provides tools for JSON-to-UBL mapping and offline validation via
npx @getpeppr/cli validate.
Working Examples
Initializing the getpeppr SDK and sending an invoice by mapping internal data to a provider JSON shape.
import { Peppol } from "@getpeppr/sdk";
export const peppol = new Peppol({
apiKey: process.env.GETPEPPR_API_KEY!,
});
async function sendInvoice(invoice: YourInvoice) {
return peppol.invoices.send({
number: invoice.number,
date: invoice.date,
dueDate: invoice.dueDate,
currency: invoice.currency ?? "EUR",
buyerReference: invoice.buyerReference ?? invoice.recipient.reference,
to: {
name: invoice.recipient.legalName,
peppolId: invoice.recipient.peppolId,
street: invoice.recipient.street,
city: invoice.recipient.city,
postalCode: invoice.recipient.postalCode,
country: invoice.recipient.country,
vatNumber: invoice.recipient.vatNumber,
},
lines: invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice import { webhooks } from "@getpeppr/sdk";
invoice .map((line) => ({
description: line description,
quantity: line quantity,
unitPrice: line unitPrice,
vatRate: line vatRate,
})),
});
}
Handling asynchronous delivery statuses via signed webhooks.
import express from "express";
import { webhooks } from "@getpeppr/sdk";
app post("/webhooks/getpeppr", express raw({ type: "*/*" }), async (req res) => {
try { const event = await webhooks constructEvent( req body toString("utf8")), req headers["getpeppr-signature"] as string, process env GETPEPPR_WEBHOOK_SECRET! "> switch (event type) { case "invoice sent": case "invoice accepted": markDeliveredInYourApp(event data invoiceId); break; case "invoice refused": case "invoice error": flagForReview(event data invoiceId, event); break; case "invoice paid": markPaid(event data invoiceId); break; } res sendStatus(200); } catch { res sendStatus(400); }"});
Practical Applications
References:
Continue reading
Next article
Why Qualified Candidates Fail the ATS: The Hidden Gap in Modern Hiring
Related Content
Full Stack Authentication in 2026: Next.js, Better Auth, and Drizzle ORM
Build a modern, type-safe authentication system using Next.js, Better Auth, and Drizzle ORM to eliminate boilerplate and manual session handling in 2026.
Scalable i18n Testing in Cypress: Semantic Assertions via i18next Integration
Sebastian Clavijo Suero demonstrates how integrating i18next into Cypress prevents test failures by asserting translation keys instead of fragile hardcoded strings.
How to Build a Zero-Cost Landing Page Stack for Business Validation
Reduce monthly SaaS costs from $99 to $0 by deploying high-performance static landing pages using AI-generated HTML and GitHub Pages.