Generate beautiful PDF invoices from a simple TOML config - straight from the CLI.
Published as @depoledna/invoiceit.
npx @depoledna/invoiceit init # create a starter invoice.toml (+ schema)
npx @depoledna/invoiceit generate invoice.toml # -> 2026-001.pdfOr install it globally, after which the command is just invoice:
npm i -g @depoledna/invoiceit
invoice generate invoice.toml -o acme.pdfAn invoice is one TOML file. Minimal example:
[invoice]
number = "2026-001"
currency = "EUR"
locale = "en-US" # controls number/date formatting (e.g. "de-DE")
[issuer]
name = "Your Company Ltd."
address = ["221B Baker Street", "London", "UK"]
payment = "IBAN: GB00 ... · BIC: ..."
# logo = "logo.png" # optional PNG/JPG, relative to this file
[client]
name = "Acme Corporation"
address = ["100 Industrial Way", "Springfield", "USA"]
[[items]]
description = "Consulting"
quantity = 10
unit = "h"
unit_price = 120.00
tax_rate = 0.19 # 19% VAT
[theme]
accent = "#4F46E5" # header/table colorDefaults: date = today, due_days = 14, currency = EUR, locale = en-US,
quantity = 1, tax_rate = 0. See examples/invoice.toml for a full one.
- Currency - set
currencyto any ISO 4217 code (USD,GBP,JPY,CHF,CZK, …). The symbol, decimal places, and grouping come from the currency + locale (e.g.JPYshows no decimals automatically). Usecurrency_display = "code"to showCZKinstead of theKčsymbol ("symbol"is the default;"narrowSymbol"is also available). - Number/date formatting - set
locale(BCP-47), e.g.de-DEgives1.800,00 €and27. Juli 2026. - Label language - set
languagefor the invoice's static text. Built in:en,de,fr,es,it,nl,cs(Czech;czalso accepted). Defaults to the locale's language. - Any other language / wording - override individual strings under
[labels](e.g.tax_name = "GST"). Keys:title,invoice_no,issue_date,due_date,from,bill_to,description,qty,unit_price,tax,amount,subtotal,total,notes,payment_details,tax_name,page.
Write {variable} in any text field and it resolves when the invoice is
generated. Everything derives from the issue date, so a config you reuse every
month stays correct without editing dates by hand.
[custom]
"Service period" = "{month_start-1m:iso} - {month_end-1m:iso}"
# → 2026-07-01 - 2026-07-31
[invoice]
number = "{date:yyyy}-{date:MM}-001" # → 2026-08-001, and names the PDF
[[items]]
description = "Retainer, {month_start-1m:MMMM yyyy}" # → Retainer, July 2026Variables — with date = "2026-08-05":
| Variable | Value |
|---|---|
{date} |
issue date (invoice.date, or today) - Aug 5, 2026 |
{due_date} |
issue date + due_days - Aug 19, 2026 |
{today} |
the real today, even when date is pinned |
{month_start} |
Aug 1, 2026 |
{month_end} |
Aug 31, 2026 |
Offsets — +N / -N with d (days), m (months), y (years):
{date+7d}, {date-1m}, {due_date+30d}, {month_start-1m}.
Month and year offsets never overflow: {month_end-1m} from Mar 31 is Feb 28,
and {month_end+1m} from Feb 28 is Mar 31.
Where they work: invoice.number, invoice.notes, issuer.payment, item
description, and the values in [custom], [issuer.custom],
[client.custom]. Everywhere else a brace is just a brace. Write {{ for a
literal {. An unknown variable is a config error with a "did you mean" hint,
not a silent blank.
date_format sets how the issue and due dates print, and is the default for
date variables. A variable can override it after a : - {date:iso}.
[invoice]
date_format = "dd.MM.yyyy"| Preset | en-US | Preset | en-US |
|---|---|---|---|
long |
August 5, 2026 | month |
August |
medium (default) |
Aug 5, 2026 | month_short |
Aug |
short |
8/5/26 | year |
2026 |
iso |
2026-08-05 |
Presets follow locale, so long reads correctly in every language
(5. August 2026, 5 août 2026).
Anything that isn't a preset name is a pattern:
| Token | Output | Token | Output |
|---|---|---|---|
yyyy |
2026 | MMM |
Aug |
yy |
26 | MM |
08 |
MMMM |
August | M |
8 |
dd |
05 | d |
5 |
"dd.MM.yyyy" → 05.08.2026, "d MMMM yyyy" → 5 August 2026. Month names
still follow locale. Non-letters pass through as separators; to keep a
literal word, quote it: "d 'of' MMMM".
Three places for free-form label/value lines:
[custom] # header meta (PO number, project, ...)
"PO Number" = "PO-2026-0042"
[issuer.custom] # extra lines under the contractor (left) block
"Reg. No." = "HRB 123456 B"
[client.custom] # extra lines under the contracted (right) block
"Attn." = "Jane Doe"[columns] # left | center | right (defaults: text left, numbers right)
description = "left"
qty = "center"
amount = "right"- Config -
src/config/loads the TOML and validates it with Zod, so a typo becomes a readable error, not a broken PDF. - Engine -
src/engine/does all money math in integer cents and groups tax by rate, so the printed breakdown always sums to the printed total. It also resolves date variables, right after the config loads - which is whyinvoice.numbercan contain one and still name the PDF. - PDF -
src/pdf/renders a React-PDF template with the bundled Inter font, so invoices look identical on every machine (no system fonts required).
invoice init also writes invoice.schema.json and adds a #:schema hint at
the top of the TOML, so editors that support it (e.g. the "Even Better TOML"
VS Code extension) give you autocomplete and inline validation. Regenerate
it anytime with invoice schema.
Configs are validated strictly: unknown keys are rejected with a "did you
mean" suggestion, so [isuer] or unit_prise surfaces immediately instead of
being silently ignored.
npm install
npm run dev generate examples/invoice.toml # run from source (tsx)
npm run build # bundle to dist/ (tsup)
npm run typecheckTypeScript · commander (CLI) · Zod (validation) · smol-toml · @react-pdf/renderer.