npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@financica/stripe-ubl

v0.3.0

Published

Convert Stripe invoices and credit notes into Peppol BIS Billing 3.0 UBL documents.

Readme

@financica/stripe-ubl

Convert Stripe invoices and credit notes into Peppol BIS Billing 3.0 UBL documents.

This is the vendor-neutral glue between Stripe's data model and the Peppol standard: it turns a Stripe.Invoice or Stripe.CreditNote into a conformant UBL XML string. It does not talk to any access point — hand the XML to whichever Peppol access point you use (e.g. @financica/scrada-client's sendOutboundDocument). Because the output is standard UBL rather than a vendor's proprietary JSON, swapping access points is a transport change, not a rewrite.

Stripe.Invoice ──@financica/stripe-ubl──▶ UBL (BIS3 XML) ──any access point──▶ Peppol

For the reverse direction (parsing inbound UBL), see @financica/ubl. That package is pure UBL; this one is the Stripe-specific glue.

Installation

npm install @financica/stripe-ubl stripe

stripe is a peer dependency — install whichever Stripe SDK version your app already uses (≥18). There are no runtime dependencies; the UBL serializer is self-contained.

Usage

Sending a Stripe invoice via Peppol

import Stripe from "stripe";
import {
	buildUblInvoiceFromStripeInvoice,
	buildPdfAttachment,
	type UblSupplier,
} from "@financica/stripe-ubl";
import { ScradaApiClient } from "@financica/scrada-client";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// 1. Retrieve the invoice with the right `expand` so per-line VAT info is
//    available under either the legacy `tax_amounts` or the newer `taxes` shape.
const invoice = await stripe.invoices.retrieve(invoiceId, {
	expand: [
		"lines.data.tax_amounts.tax_rate",
		"lines.data.taxes.tax_rate_details.tax_rate",
	],
});

// 2. Resolve the supplier from your own data store.
const supplier: UblSupplier = {
	name: "Acme BE",
	countryCode: "BE",
	address: {
		line1: "Rue de la Loi 16",
		city: "Brussels",
		postal_code: "1000",
		country: "BE",
	},
	companyNumber: "0800279001",
	vatNumber: "BE0800279001",
	vatStatus: 1, // 1 = Subject to VAT, 2 = Not subject, 3 = Small business / franchise
	peppolID: "0208:0800279001",
};

// 3. Optionally embed the rendered PDF.
const pdf = await fetch(invoice.invoice_pdf!).then((r) => r.arrayBuffer());
const attachment = buildPdfAttachment({
	filename: `${invoice.number}.pdf`,
	bytes: new Uint8Array(pdf),
});

// 4. Build the UBL and hand it to your access point.
const ubl = buildUblInvoiceFromStripeInvoice({ invoice, supplier, attachment });

const scrada = new ScradaApiClient({
	apiKey: process.env.SCRADA_API_KEY!,
	password: process.env.SCRADA_PASSWORD!,
});
const documentId = await scrada.sendOutboundDocument(scradaCompanyId, ubl, {
	idempotencyKey: invoice.id,
});

Sending a Stripe credit note

Credit notes don't carry the customer party themselves — they reference the original invoice. Pass both; the parent invoice number is emitted as the cac:BillingReference (BT-25).

import { buildUblCreditNoteFromStripeCreditNote } from "@financica/stripe-ubl";

const creditNote = await stripe.creditNotes.retrieve(creditNoteId, {
	expand: ["invoice.customer", "lines.data.taxes.tax_rate_details.tax_rate"],
});
const invoice =
	typeof creditNote.invoice === "string"
		? await stripe.invoices.retrieve(creditNote.invoice)
		: creditNote.invoice;

const ubl = buildUblCreditNoteFromStripeCreditNote({ creditNote, invoice, supplier });

Building the model without serializing

buildUblInvoiceFromStripeInvoice is serializeUblDocument(buildUblInvoiceDocument(...)). Use the document builder when you want to inspect or tweak the model before serializing:

import {
	buildUblInvoiceDocument,
	serializeUblDocument,
	sanitizeUblDocumentForAudit,
} from "@financica/stripe-ubl";

const doc = buildUblInvoiceDocument({ invoice, supplier });
auditLog(sanitizeUblDocumentForAudit(doc)); // redacts attachment base64
const ubl = serializeUblDocument(doc);

What gets reconciled

Stripe sometimes reports per-line tax differently from the document header (rounding, distributed coupons, prorations). This library reconciles those into a UBL document that is internally consistent and EN 16931-conformant:

  • Line nets are reconciled against Stripe's authoritative total_excluding_tax; any sub-cent difference is pushed into the largest line (BR-CO-13 / BR-S-08 stay consistent bottom-up).
  • The VAT breakdown is grouped by (category, rate), and each category's tax amount is derived as taxable × rate / 100 rounded to two decimals (BR-CO-17) — not summed from upstream tax cents. This can differ by a cent from the figure Stripe reported, which is an unavoidable artifact of representing a cents-rounded system as a rate-based VAT breakdown; the resulting document validates.
  • Per-line VAT falls back from tax_amounts to taxes when only the newer shape is populated, so the rate isn't silently lost on accounts mid-migration.
  • Discounted lines use the post-discount net as both the VAT base and the line net, so a discounted standard-rated line keeps its true rate (e.g. 21%, not 14.70%). Line discounts are folded into the net rather than emitted as a cac:AllowanceCharge.
  • Fully-discounted lines read the rate from the expanded tax_rate.percentage so a 100%-discounted standard-rated line stays category S instead of collapsing to zero-rated.

VAT categories & vatStatus

Lines are classified into UNCL5305 VAT categories from the Stripe tax data:

| Category | Meaning | From | | --- | --- | --- | | S | Standard rate | a positive rate | | Z | Zero-rated | rate 0 / zero_rated | | E | Exempt | customer_exempt, product_exempt, not_subject_to_tax, … | | AE | Reverse charge | reverse_charge |

EN 16931 requires an exemption reason on the non-S/Z categories, which the library fills in automatically.

supplier.vatStatus covers the seller side:

| Value | Meaning | | --- | --- | | 1 | Subject to VAT — line categories come from the data (the normal case) | | 2 | Not subject to VAT | | 3 | Small business / franchise (e.g. Belgian Article 56bis) |

For 2 and 3, every line is coerced to category E with an appropriate exemption reason so no VAT is reported.

Surface

// High-level (Stripe → UBL XML string)
buildUblInvoiceFromStripeInvoice(params): string
buildUblCreditNoteFromStripeCreditNote(params): string

// Mid-level (Stripe → UblDocument model)
buildUblInvoiceDocument(params): UblDocument
buildUblCreditNoteDocument(params): UblDocument

// Serializer (UblDocument → XML) + audit helper
serializeUblDocument(doc): string
sanitizeUblDocumentForAudit(doc): UblDocument

// Party builders
buildSupplierParty(supplier): UblParty
buildCustomerPartyFromStripeInvoice(invoice): { customer, customerName }

// Lines, VAT breakdown, reconciliation
buildInvoiceLines(invoice) / buildCreditNoteLines(creditNote, fallbackName)
buildTaxTotals(lines) / reconcileLinesToExclTotal(lines, authoritativeExclVat)
resolveTaxCategoryFromTaxAmounts(taxAmounts, rate)
taxCategoryFromReasonOrRate({ taxCategoryId?, taxabilityReason, rate })

// Stripe tax extraction
getInvoiceLineTaxAmounts(line) / getCreditNoteLineTaxAmounts(line)
getInvoiceLineDiscountAmountCents(line)

// Identifiers + address + attachment
extractCustomerTaxIdentifiers(stripeTaxIds)
listPeppolReceiverIdentifierCandidates(customer)
normalizeCompanyNumberForCountry(country, number)
resolveCompanyIdScheme({ countryCode, companyNumber })
parsePeppolEndpoint("0208:0800279001")
normalizeAddress(address, fallbackCountryCode, fallbackLine?)
buildPdfAttachment({ filename, bytes, id? })

// Low-level XML primitives + UBL constants
el / serializeDocument
UBL_CUSTOMIZATION_ID, UBL_PROFILE_ID, INVOICE_TYPE_CODE, …

Conformance

The output targets EN 16931 + Peppol BIS Billing 3.0 and is built to satisfy the calculation rules (BR-CO-10/13/15/17, BR-S-08, …). It is not yet wired to the official EN 16931 / Peppol schematron — if you depend on guaranteed conformance, validate the emitted XML against the published schematron in CI (and your access point will validate on ingest). Some optional constructs (line-level AllowanceCharge, PaymentMeans, prepaid amounts) are intentionally not emitted yet.

License

MIT