Skip to content

Send an Invoice

Send invoices to the Peppol network with attachments, allowances, charges, and delivery details.

POST /api/v1/invoices #

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.

Line Items

Each line needs description, quantity, unitPrice, and vatRate. Amounts are calculated automatically — no need for manual totals.

Payment

Add paymentTerms and paymentIban to include payment instructions. Strongly recommended for Belgian and French mandates.

Parameters

NameTypeRequiredDescription
numberstringRequiredUnique invoice number (e.g. INV-2026-042)
toBuyerPartyRequiredBuyer details: name, peppolId, street, city, postalCode, country
linesInvoiceLine[]RequiredLine items with description, quantity, unitPrice, vatRate
fromPartyOptionalDeprecated local metadata. The gateway determines the seller from your API key.
datestringOptionalISO 8601 invoice date (defaults to today)
dueDatestringOptionalISO 8601 payment due date
currencystringOptionalISO 4217 currency code (default: "EUR")
paymentTermsstringOptionalPayment terms description
paymentIbanstringOptionalIBAN for bank transfer
attachmentsAttachment[]OptionalEmbedded or external file attachments
allowancesAllowanceCharge[]OptionalDocument-level discounts
chargesAllowanceCharge[]OptionalDocument-level surcharges
deliveryDeliveryOptionalDelivery date and address
invoicePeriodInvoicePeriodOptionalBilling period (start/end dates)
notestringOptionalFree-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: "Wayne Enterprises NV",
    peppolId: "0208:BE0123456789",
    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.

Max embedded attachment size is 10 MB decoded per attachment. For larger files, use an external URL. Server-side validation applies a stricter 2 MB decoded cap before UBL generation.
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: "Globex NV", peppolId: "0208:BE0987654321", 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.

Allowances and charges follow BIS 3.0 business groups BG-20 (document allowances), BG-21 (document charges), BG-27 (line allowances), and BG-28 (line charges). See Type Definitions for the full interface.
import { Peppol } from "@getpeppr/sdk";

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

const result = await peppol.invoices.send({
  number: "INV-2026-044",
  to:   { name: "Globex NV", peppolId: "0208:BE0987654321", 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.

For SaaS and subscription billing, always include 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: "Globex NV", peppolId: "0208:BE0987654321", 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.

HTTP 422
{
  "error": "Validation failed: street1 not provided, postalCode not provided",
  "code": "buyer_address_incomplete"
}

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.

HTTP 422
{
  "error": "Recipient 0208:BE9999999999 not found in Peppol Directory",
  "code": "recipient_not_in_directory"
}

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.

HTTP 429 (Retry-After: 30)
{
  "error": "Rate limit exceeded. Retry after 30 seconds.",
  "code": "rate_limited"
}
The SDK exposes PeppolValidationError (carries the structured validation object with field paths and suggestions) and PeppolApiError (carries statusCode and retryAfterMs). Check err instanceof to branch reliably on error type.