Send an Invoice
Send invoices to the Peppol network with attachments, allowances, charges, and delivery details.
Send a Peppol invoice. The API validates the payload against BIS 3.0 business rules, generates UBL XML, and delivers it to the buyer's access point.
Parties
Your API key determines the seller/legal entity. The payload must include to (buyer) with name, peppolId, street, city, postalCode, and country. The deprecated from field is ignored by the gateway and kept only for local/offline compatibility.
from field. Sending without one returns a 422 with a hint pointing you there; production standard-key sends use peppol_identity_incomplete.9925:BE0314595348 (SPF Economie, the standard Peppol test receiver) as the to.peppolId. See the sandbox guide.Line Items
Each line needs description, quantity, unitPrice, and vatRate. An optional vatCategory sets the UBL tax category (defaults to standard VAT). Amounts are calculated automatically — no need for manual totals.
vatCategory accepts "S" (standard), "Z" (zero-rated), "E" (exempt), "AE" (reverse charge), "K" (intra-community), "G" (export) and "O" (outside scope). Codes are case-sensitive: "AE" is reverse charge, "ae" is rejected. Omit the field to take the default — a value you supply is never silently corrected.
GET /v1/identity and copy sandboxFirstSend.line onto each line. It returns either O/0 for a routing-only sender or AE/0 for a sender carrying a tax identifier. The CLI quickstart performs this preflight automatically and blocks before submission when the identity is not ready.Payment
Add paymentTerms and paymentIban to include payment instructions. Strongly recommended for Belgian and French mandates.
GET /v1/invoices/{id}/as/original answers 404 on a document that is perfectly healthy — the stored copy is still being registered. Fetching it synchronously right after a send therefore looks like a failure; poll rather than assuming one retry is enough, since no bound on that window is guaranteed. It does not wait for delivery: the document is downloadable well before the receiving access point has signed for it. To know that delivery actually happened, read the status from GET /v1/invoices/{id} — available immediately — or wait for the invoice.sent webhook.Parameters
| Name | Type | Required | Description |
|---|---|---|---|
number | string | Required | Unique invoice number (e.g. INV-2026-042) |
to | BuyerParty | Required | Buyer details: name, peppolId, street, city, postalCode, country |
lines | InvoiceLine[] | Required | Line items with description, quantity, unitPrice, vatRate |
from | Party | Optional | Deprecated local metadata. The gateway determines the seller from your API key. |
date | string | Optional | ISO 8601 invoice date (defaults to today) |
dueDate | string | Optional | ISO 8601 payment due date |
currency | string | Optional | ISO 4217 currency code (default: "EUR") |
paymentTerms | string | Optional | Payment terms description |
paymentIban | string | Optional | IBAN for bank transfer |
attachments | Attachment[] | Optional | Embedded or external file attachments |
allowances | AllowanceCharge[] | Optional | Document-level discounts |
charges | AllowanceCharge[] | Optional | Document-level surcharges |
delivery | Delivery | Optional | Delivery date and address |
invoicePeriod | InvoicePeriod | Optional | Billing period (start/end dates) |
note | string | Optional | Free-text note for the buyer |
import { Peppol } from "@getpeppr/sdk";
const peppol = new Peppol({ apiKey: "sk_live_..." });
const result = await peppol.invoices.send({
number: "INV-2026-042",
buyerReference: "PO-2026-007",
// Buyer
to: {
name: "ACMEDIA",
peppolId: "0208:0685660237",
street: "Avenue Louise 54",
city: "Brussels",
postalCode: "1050",
country: "BE",
},
// Line items
lines: [
{ description: "Arc Reactor Maintenance Q1", quantity: 1, unitPrice: 50_000, vatRate: 21 },
{ description: "Vibranium Shield Polish", quantity: 3, unitPrice: 250, vatRate: 21 },
],
// Payment
paymentTerms: "Net 30 days",
paymentIban: "BE68539007547034",
// Optional
date: "2026-03-01",
dueDate: "2026-03-31",
note: "Thank you for your business!",
});
console.log(`Sent! ID: ${result.id}, Status: ${result.status}`);Attachments
Attach supporting documents to your invoices — PDF copies, timesheets, contracts, or any file. Supports both embedded (base64) and external URL references.
Embedded
Provide filename, mimeType, and raw base64-encoded content without a data URI prefix. The file is included directly in the UBL XML.
External URL
Provide a url instead. The buyer's access point will fetch the document. Make sure the URL is publicly accessible.
import { Peppol } from "@getpeppr/sdk";
import { readFileSync } from "fs";
const peppol = new Peppol({ apiKey: "sk_live_..." });
const result = await peppol.invoices.send({
number: "INV-2026-043",
to: { name: "ACMEDIA", peppolId: "0208:0685660237", street: "Rue de la Loi 200", city: "Brussels", postalCode: "1000", country: "BE" },
lines: [
{ description: "Consulting Q1", quantity: 40, unitPrice: 125, vatRate: 21 },
],
// Embed a PDF copy of the invoice
attachments: [
{
id: "ATT-001",
filename: "invoice-2026-043.pdf",
mimeType: "application/pdf",
content: readFileSync("./invoice.pdf").toString("base64"),
},
// Or reference an external document
{
id: "ATT-002",
description: "Detailed timesheet",
url: "https://acme.com/docs/timesheet-q1.pdf",
},
],
});Allowances & Charges
Apply discounts (allowances) and surcharges to invoices at both the document level and line level.
Document-level
Use allowances for discounts and charges for surcharges that apply to the entire invoice. Each requires reason, amount, and vatRate.
Line-level
Add allowances or charges to individual line items. Line-level adjustments only need reason and amount.
import { Peppol } from "@getpeppr/sdk";
const peppol = new Peppol({ apiKey: "sk_live_..." });
const result = await peppol.invoices.send({
number: "INV-2026-044",
to: { name: "ACMEDIA", peppolId: "0208:0685660237", street: "Rue de la Loi 200", city: "Brussels", postalCode: "1000", country: "BE" },
lines: [
{
description: "Consulting hours",
quantity: 40,
unitPrice: 125,
vatRate: 21,
// Line-level discount
allowances: [{ reason: "Loyalty discount", amount: 200 }],
},
],
// Document-level discount (applies to entire invoice)
allowances: [
{ reason: "Early payment discount (2%)", amount: 100, vatRate: 21 },
],
// Document-level surcharge
charges: [
{ reason: "Express delivery fee", amount: 50, vatRate: 21 },
],
});Delivery & Period
Specify delivery details and billing periods for invoices that cover goods delivery or service periods.
Delivery
Add a delivery object with an optional date and address. Some EU countries (e.g. Italy, Spain) require delivery information on invoices.
Invoice Period
Use invoicePeriod with startDate and endDate for subscriptions, retainers, or any service billed over a time range.
invoicePeriod — it's required by some national rules and improves reconciliation for your buyers.import { Peppol } from "@getpeppr/sdk";
const peppol = new Peppol({ apiKey: "sk_live_..." });
const result = await peppol.invoices.send({
number: "INV-2026-045",
to: { name: "ACMEDIA", peppolId: "0208:0685660237", street: "Rue de la Loi 200", city: "Brussels", postalCode: "1000", country: "BE" },
lines: [
{ description: "Server hardware", quantity: 10, unitPrice: 2500, vatRate: 21 },
],
// Delivery details (mandatory in some EU countries)
delivery: {
date: "2026-03-15",
address: {
street: "Rue de la Science 14",
city: "Brussels",
postalCode: "1000",
country: "BE",
},
},
// Billing period (common for SaaS / subscriptions)
invoicePeriod: {
startDate: "2026-01-01",
endDate: "2026-03-31",
},
});Common Errors
Most failures on this endpoint fall into one of these categories. The API returns structured JSON with a stable error field and (where available) a machine-readable code for client-side branching. See Error Handling for the full reference.
Validation failed (422)
One or more fields failed Peppol BIS 3.0 validation. Common causes: missing buyer address (BR-50/51/53), invalid VAT rate (BR-CO-17), malformed Peppol ID, or unknown currency code.
{
"error": "Buyer address is required: street, city, and postalCode must be provided in the 'to' object"
}Recipient not on the Peppol network (422)
Returned when x-validate-recipient: strict is set and the recipient's Peppol ID is not registered. Verify with GET /v1/directory/{scheme}/{id} first.
{
"error": "Recipient 0208:0685660237 not found in Peppol Directory. The participant may not be registered on the Peppol network."
}Rate limited (429)
Sandbox limit is 10 req/min per key. The response includes a Retry-After header with the seconds to wait. The SDK retries automatically with exponential backoff.
{
"error": "Rate limit exceeded. Try again later."
}PeppolValidationError (carries the structured validation object with field paths and suggestions), PeppolProtocolError (a 2xx body missing a field the contract requires, since SDK 4.0.0), and PeppolApiError (carries statusCode and retryAfterMs). All three extend PeppolError; see Error Handling for the one failure that sits outside the hierarchy. Check err instanceof to branch reliably on error type.