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
Automating Medium Reading List Syndication via Zenndra API
Learn how to sync Medium reading lists into LMS or newsletters using the Zenndra API for automated content curation.
Engineering a Real-Time Robot Battle Simulator: Lessons in Performance and Language Design
A technical deep dive into Logic Arena, featuring a custom scripting language and the resolution of a 3,862ms scripting bottleneck.
AI SDLC Transformation — Part 1: Where to Start?
Engineering leaders must prioritize clarity and structured approaches when integrating AI into the software development lifecycle (SDLC), focusing on project type, metrics, and systemic thinking for sustainable transformation.