Skip to content

Your first invoice

This page builds a complete Peppol invoice with createInvoice() and writes it as UBL XML. Every recipe extends the same input.

Describe the invoice

An InvoiceInput holds the facts of the business case: the invoice number (BT-1), issue date (BT-2), type code (BT-3), currency (BT-5), a buyer reference (BT-10, or an order reference, BT-13, which Peppol accepts instead), the seller (BG-4) and the buyer (BG-7) with their electronic addresses, and at least one line (BG-25):

ts
export const baseInvoice: InvoiceInput = {
  id: 'Snippet1',
  issueDate: '2017-11-13',
  dueDate: '2017-12-01',
  invoiceTypeCode: '380',
  documentCurrencyCode: 'EUR',
  buyerReference: '0150abc',
  accountingSupplierParty: {
    party: {
      endpointId: { schemeId: '0088', value: '7300010000001' },
      postalAddress: { cityName: 'London', country: { identificationCode: 'GB' } },
      partyTaxScheme: [{ companyId: 'GB1232434' }],
      partyLegalEntity: { registrationName: 'SupplierOfficialName Ltd' },
    },
  },
  accountingCustomerParty: {
    party: {
      endpointId: { schemeId: '0002', value: '4598375937' },
      postalAddress: { cityName: 'Stockholm', country: { identificationCode: 'SE' } },
      partyLegalEntity: { registrationName: 'Buyer Official Name' },
    },
  },
  invoiceLine: [
    {
      id: '1',
      invoicedQuantity: { value: 7, unitCode: 'DAY' },
      item: { name: 'Consulting', classifiedTaxCategory: { id: 'S', percent: 25 } },
      price: { priceAmount: 400 },
    },
  ],
};

Decimals may be numbers or strings: 400 and '400.00' both work, and the precision you write is kept. A schemeId on an identifier is the code of its identification scheme; 0088 is a GLN.

Create it

ts
const invoice = createInvoice(baseInvoice);

const lineAmount = invoice.invoiceLine[0]?.lineExtensionAmount; // { value: '2800.00', currencyId: 'EUR' }
const vat = invoice.taxTotal[0]?.taxAmount; // { value: '700.00', currencyId: 'EUR' }
const payable = invoice.legalMonetaryTotal.payableAmount; // { value: '3500.00', currencyId: 'EUR' }

createInvoice() adds what the syntax requires and you did not state:

  • customizationId and profileId: Peppol BIS Billing 3.0, billing process 01;
  • taxScheme: { id: 'VAT' } on the party tax scheme and on every tax category;
  • currencyId on every amount, from documentCurrencyCode.

It then computes each amount that is absent: each line's lineExtensionAmount (quantity × price ÷ base quantity, plus line charges, minus line allowances), taxTotal with one breakdown line per VAT category and rate, and legalMonetaryTotal. Computed amounts are rounded to two decimals the way the Schematron rounds.

The builder never overrides what you set

An amount you state is kept as written, even when it is wrong. That keeps parsed documents faithful when you rebuild them and lets tests produce deliberately invalid documents. validate() tells you whether the amounts add up.

Write the XML

ts
const xml = serialize(invoice);
// <?xml version="1.0" encoding="UTF-8"?>
// <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" ...>
//   <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
//   <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
//   <cbc:ID>Snippet1</cbc:ID>
//   ...

The serializer writes elements in the order of the UBL schema, whatever the order of your object's keys, and escapes text. It does not validate: Parsing, validating and serializing covers that.

Next steps

Each recipe adds one area of the invoice to baseInvoice: the parties, lines, VAT, allowances and charges, payment and more.

MIT licensed. Specification artefacts belong to OpenPEPPOL, CEN and OASIS.