simpleinvoicing
v3.0.1
Published
Invoice generator SDK — generate PDF invoices in one call from Node, Browser, Bun or Deno. Multi-currency (EUR/USD/GBP/CZK/PLN/HUF/CHF), multi-language labels (EN/SK/CZ/HU/PL/DE), VAT-aware, EU-ready. Optional reusable supplier profiles, client directory,
Keywords
Readme
simpleinvoicing
The official Node, Browser, Bun and Deno SDK for Generate Invoice — a hosted invoice generator and PDF billing API. Turn a JSON payload into a polished PDF invoice in one await. Multi-currency, multi-language, EU-ready, VAT-aware. Free tier, no credit card.
npm install simpleinvoicingimport { createClient } from 'simpleinvoicing';
const si = createClient({ licenseKey: process.env.SI_KEY! });
const pdf = await si.invoices.generate({
supplier: { name: 'Acme Ltd' },
purchaser: { name: 'Globex' },
invoiceDetails: { invoiceNr: 'INV-2026-001', issueDate: '2026-06-17', dueDate: '2026-07-01', currency: 'EUR' },
items: [{ item: 'Design retainer', quantity: 1, unitCost: 2400 }],
});Looking for: invoice generator · PDF invoice API · invoicing library for Node.js · billing API · automated invoicing · invoice maker · receipt generator · VAT invoice / EU invoicing / IČO / DIČ · multi-currency invoicing (EUR / USD / GBP / CZK / PLN / HUF / CHF) · multi-language invoice templates (EN / SK / CZ / HU / PL / DE) · recurring billing automation · SaaS invoicing · freelancer invoicing · white-label invoicing SDK · REST API for invoices.
What you can do with it
- 📄 Generate PDF invoices in roughly a second from a single JSON payload — no PDFKit, no html-to-pdf wrangling, no Puppeteer to ship
- 💶 Multi-currency out of the box — EUR, USD, GBP, CZK, PLN, HUF, CHF (extend on request)
- 🌍 Multi-language labels on the PDF — English (US / GB), Slovak, Czech, Hungarian, Polish, German; localised date formats
- 🧾 VAT-aware — invoice-level or per-line tax rates, automatic totals breakdown, EU-friendly supplier / purchaser blocks (business ID, VAT ID, registry note)
- 🏢 Reusable supplier profiles — IBAN, business ID, VAT ID, logo and design tokens saved once and referenced by id
- 📇 Client directory + item catalog — invoice repeat customers without retyping a thing; sorted by "recently used"
- #️⃣ Custom invoice numbering — per profile or per client, with
{YYYY},{MM},{####}style placeholders - 📜 Saved history —
list/get/setPayment(paid|cancelled)/ re-download; tabs for open / paid / drafts / API vs web - 🎨 Custom branding — logo, accent color, body color, font (Roboto / Inter / Lora), optional "PAID" stamp, hide-branding flag
- 🔌 Runs anywhere — Node 18+, Bun, Deno, modern browsers; ESM + CJS; subpath
simpleinvoicing/browserfor client-side blob helpers - 🆓 Generous free tier — 2 invoices / month forever-free; €2.99/mo for 100, €9.99/mo unmetered (incl. VAT)
When to reach for it
- SaaS billing automation — cron-fire monthly invoices from your subscription engine after each Stripe payment
- Freelancers, agencies, studios — issue branded invoices directly from your existing stack instead of switching to a separate tool
- E-commerce, marketplaces, order systems — render an invoice per order, attach it to the order-confirmation email
- Accounting & ERP integrations — generate the PDF, hand it off to your bookkeeping pipeline (Pohoda, MoneyS3, Stripe Tax, …)
- Internal admin dashboards — give your ops team an "Issue invoice" button without building an editor
- White-label invoicing — flip on
hideBrandingand ship invoices under your own brand
There's also a hosted web editor at https://invoice.codurra.com if you (or your customers) want to issue invoices without touching code — both surfaces share the same engine, the same history, and the same license key.
What's required vs. optional?
| Capability | Required? | Notes |
| ------------------------------------ | ----------------- | ------------------------------------------------------------ |
| License key | ✅ Required | Get one at https://invoice.codurra.com/dashboard/developers (free tier included). |
| invoices.generate() | ✅ Required | The only call you actually need to issue an invoice. |
| Supplier profiles (si.profiles.*) | ⬜ Optional | Convenience. Saves your billing identity once so you don't re-type it on every invoice. Pass profileId to use it. |
| Clients (si.clients.*) | ⬜ Optional | Same idea for purchaser. Pass clientId to use one. |
| Item catalog (si.items.*) | ⬜ Optional | Reusable products / services. You can always pass line items inline on every invoice. |
| Saving invoices to history (save: true) | ⬜ Optional | Without it, the PDF is returned but nothing is stored. With it, the invoice appears in your dashboard and counts toward your monthly quota. |
The minimum viable use is: install + license key + one call to
invoices.generate(...)with inline supplier/purchaser/items. Everything else exists to make your code shorter when you're invoicing the same entities repeatedly.
Quick start (zero setup, no stored data)
import { createClient } from 'simpleinvoicing';
const si = createClient({ licenseKey: process.env.SI_KEY! });
const pdf = await si.invoices.generate({
// ── REQUIRED ──────────────────────────────────────────────
supplier: { name: 'Acme Ltd' }, // required if no profileId
purchaser: { name: 'Globex' }, // required if no clientId
invoiceDetails: {
invoiceNr: 'INV-2026-001', // required
issueDate: '2026-06-15', // required
dueDate: '2026-06-29', // required
currency: 'EUR', // required
},
items: [ // required, non-empty
{ item: 'Design retainer', quantity: 1, unitCost: 2400 }, // item + quantity + unitCost required per line
],
// ── OPTIONAL ──────────────────────────────────────────────
// paymentDetails: { iban: '…', bankName: '…' },
// invoiceSettings: { save: true, locale: 'EN_GB', accentColor: '#0d8a4f' },
});
console.log(pdf.invoiceNr, pdf.totals?.total);To write the PDF to disk:
import { savePdfToFile } from 'simpleinvoicing';
await savePdfToFile(pdf, './invoices'); // writes ./invoices/INV-2026-001.pdfinvoices.generate(body) — the only required call
The body is the GenerateInvoiceBody type. See Shared types
below for Party, PaymentDetails, Design, InvoiceItem.
Top level
| Field | Type | Required | Notes |
| ----------------- | ----------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| invoiceDetails | InvoiceDetails | ✓ | See table below. |
| items | InvoiceItem[] | ✓ | Non-empty array. |
| supplier | Partial<Party> | one of | Required if no profileId. With profileId, anything here overrides the stored profile per-key. |
| profileId | number | one of | Use a stored supplier profile — fills party + payment details + design. |
| purchaser | Partial<Party> | one of | Required if no clientId. |
| clientId | number | one of | Use a stored client — fills party and bumps the client's lastUsedAt. |
| paymentDetails | PaymentDetails | | Inline override; falls through to the profile's defaults if omitted. |
| invoiceSettings | InvoiceSettings | | Controls save, returnAs, look & feel. |
| status | 'final' | 'draft' | | Default final. draft persists without rendering a PDF. |
InvoiceDetails
| Field | Type | Required | Notes |
| -------------- | --------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| issueDate | string YYYY-MM-DD | ✓ | |
| dueDate | string YYYY-MM-DD | ✓ | |
| currency | 'EUR' | 'USD' | 'GBP' | 'CZK' | 'PLN' | 'HUF' | 'CHF' | ✓ | Three-letter ISO; defaults to EUR on the server. |
| invoiceNr | string ≤40 | ✓* | * Optional only when profileId is set — then the server allocates from the profile's counter. |
| titleSuffix | string ≤40 | | Free-form subtitle (e.g. "Pro forma", "Credit note"). |
| deliveryDate | string | | |
| dueDays | integer ≥1 | | Auto-footer text: "Payment is due within X days…". |
| taxRate | number 0–100 | | Invoice-level VAT %; falls through to lines without their own taxRate. |
| notes | string ≤2000 | | Top-level note above the footer. |
| footerNote | string ≤400 | | Small extra footer line. |
| paid | boolean | | Renders the "PAID" stamp + records payment. |
InvoiceSettings
All optional — pass only what you need.
| Field | Type | Default | Notes |
| ---------------- | ------------------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
| save | boolean | false | Persist to history. Counts toward your monthly quota; lets you list / setPayment / re-download. |
| mode | 'production' | 'development' | production| development = draft watermark; doesn't increment usage. |
| returnAs | 'base64' | 'url' | base64 | Where the PDF lands: in the response body, or as a downloadable URL. |
| locale | see Design.locale | EN_US | |
| dateFormat | see Design | ISO | |
| font | see Design | Roboto | |
| logo | URL · data URL · /uploads/... | — | Max 400 KB base64. |
| logoWidth | number 40–400 | 120 | PDF points. |
| pageBackground | URL · data URL · /uploads/... | — | Max 800 KB base64. |
| accentColor | hex | #7c3aed | Title + totals card. |
| textColor | hex | #1f1438 | Body. |
| borderColor | hex | #e6dffb | Hairline rules. |
| footerText | string ≤400 | auto | Override the auto footer copy. |
| hideBranding | boolean | false | Removes "codurra" mark. |
| showPaidStamp | boolean | false | Prints "PAID". |
Note:
invoiceSettings.licenseKeyis filled in by the SDK fromcreateClient({ licenseKey }). Don't pass it yourself when using the factory.
Shared types
Used across multiple resources. Defined here once.
Party
A billing identity — a supplier or a purchaser.
| Field | Type | Required | Notes |
| -------------- | -------------- | -------- | ---------------------------------------------------- |
| name | string ≤200 | ✓ | Legal / business name shown at the top of the block. |
| address | string ≤300 | | Street, line 1. |
| city | string ≤120 | | |
| zip | string ≤20 | | ZIP / postcode. |
| country | string ≤80 | | |
| businessId | string ≤40 | | e.g. SK IČO, GB CRN. |
| taxId | string ≤40 | | e.g. SK DIČ. |
| vatId | string ≤40 | | e.g. SK IČ DPH, GB VAT. |
| email | email | | Contact mailbox. |
| phone | string ≤40 | | |
| website | string ≤200 | | |
| registryNote | string ≤300 | | Free-form registry footnote. |
PaymentDetails
Bank / wire info printed in the payment block. All fields optional.
| Field | Type | Notes |
| ---------------- | ----------- | ------------------------------ |
| paymentMethod | string ≤80 | e.g. "Bank transfer", "PayPal".|
| bankName | string ≤120 | |
| iban | string ≤60 | SEPA-style. |
| swift | string ≤40 | BIC. |
| routing | string ≤40 | US/CA routing number. |
| account | string ≤40 | US/CA account number. |
| accountType | string ≤40 | "checking" / "savings". |
| variableSymbol | string ≤40 | SK/CZ payment reference. |
| constantSymbol | string ≤40 | SK/CZ purpose code. |
| specificSymbol | string ≤40 | SK/CZ specific symbol. |
| cryptoAddress | string ≤200 | Optional crypto hint. |
Design
Visual overrides — set once on a profile (or per-client), propagated to every invoice.
| Field | Type | Default | Notes |
| ----------------- | -------------------------------------------------------------------- | ------------- | -------------------------------------- |
| locale | SK / CZ / HU / PL / DE / EN_GB / EN_US | EN_US | Labels printed on the PDF. |
| dateFormat | DMY / MDY / YMD / ISO | ISO | |
| font | Roboto / Inter / Lora | Roboto | |
| logo | URL · data URL · /uploads/... | — | Max 400 KB base64. PNG/JPEG/WebP/SVG. |
| logoWidth | number 40–400 | 120 | PDF points (1pt = 1/72 in). |
| pageBackground | URL · data URL · /uploads/... | — | Max 800 KB base64. |
| accentColor | hex #RRGGBB | #7c3aed | Title + totals card. |
| textColor | hex | #1f1438 | Body. |
| borderColor | hex | #e6dffb | Hairline rules. |
| footerText | string ≤400 | auto | Override the auto-generated footer. |
| hideBranding | boolean | false | Removes "codurra" mark. |
| showPaidStamp | boolean | false | Prints "PAID". |
InvoiceItem
One line on an invoice. items is a non-empty array of these.
| Field | Type | Required | Notes |
| -------------- | -------------------------- | -------- | ------------------------------------------------ |
| item | string ≤200 | ✓ | Line description. |
| quantity | number ≥0 | ✓ | |
| unitCost | number ≥0 | ✓ | Pre-tax, in the invoice currency. |
| description | string ≤500 | | Optional second line under the item name. |
| unit | string ≤20 | | "hour", "kg"… |
| taxRate | number 0–100 | | Per-line VAT %; falls back to invoiceDetails.taxRate. |
| discount | number ≥0 | | % or flat — controlled by discountKind. |
| discountKind | percent / flat | | Default percent. |
| periodFrom | string | | Optional subscription period; prints under the item. |
| periodTo | string | | |
| type | product / discount | | discount renders italic + subtracts from totals. |
Optional: stored entities
If you re-invoice the same supplier, client, or item often, save them once and
pass an id instead of inline data. Everything below is convenience — you
can build the same app with just invoices.generate() and inline blocks.
Profiles — your billing identities (optional)
// Optional: save your "from" identity once.
const profile = await si.profiles.create({
name: 'Acme — primary', // friendly nickname (required)
party: { name: 'Acme Ltd', vatId: 'GB123456789', country: 'UK' }, // party.name required
paymentDetails: { iban: 'GB29 NWBK 6016 1331 9268 19' }, // optional
isDefault: true, // optional
});
// Then on the invoice:
await si.invoices.generate({
profileId: profile.id, // supplier filled in from the profile
purchaser: { name: 'Globex' },
invoiceDetails: { /* … */ },
items: [/* … */],
});ProfileInput fields
| Field | Type | Required | Notes |
| ---------------------- | -------------------------------------- | -------- | --------------------------------------------------------------------------- |
| name | string ≤200 | ✓ | Internal nickname shown in the picker (independent of party.name). |
| party | Party | ✓ | The legal entity. party.name required. |
| paymentDetails | PaymentDetails | null | | Default bank info for invoices issued from this profile. |
| design | Design | null | | Per-profile visual overrides. |
| initialInvoiceFormat | string ≤80 | | Format for the auto-created number sequence, e.g. INV-{YYYY}-{####}. |
| isDefault | boolean | | Setting true clears the flag on other profiles. |
| active | boolean | | false hides from the editor picker; history intact. |
ProfileResponse adds: id: number, active: boolean, isDefault: boolean.
Clients — parties you bill (optional)
const client = await si.clients.create({
name: 'Globex', // required
party: { name: 'Globex Corporation', email: '[email protected]' }, // party.name required
});
await si.invoices.generate({
profileId: profile.id,
clientId: client.id,
invoiceDetails: { /* … */ },
items: [/* … */],
});ClientInput fields
| Field | Type | Required | Notes |
| -------- | ----------------------------------- | -------- | ------------------------------------------------------------------ |
| name | string ≤200 | ✓ | Internal nickname shown in the picker. |
| party | Party | ✓ | The legal entity. party.name required. |
| design | Design | null | | Per-client visual override; falls through to the profile's design. |
| active | boolean | | false hides from the editor picker. |
ClientResponse adds: id: number, active: boolean, lastUsedAt: string | null (auto-bumped when you issue an invoice for this client).
Items — reusable catalog (optional)
const retainer = await si.items.create({
name: 'Design retainer', // required
unitCost: 600, // required
unit: 'week', // optional
taxRate: 20, // optional
description: 'Weekly design sprint', // optional
});
// Use it on an invoice:
await si.invoices.generate({
/* … */,
items: [{ item: retainer.name, quantity: 1, unitCost: retainer.unitCost }],
});ItemInput fields
| Field | Type | Required | Notes |
| ------------- | -------------------------- | -------- | ---------------------------------------------------------------- |
| name | string ≤200 | ✓ | What shows up in the picker and on the invoice line. |
| unitCost | number ≥0 | ✓ | Pre-tax base price. |
| description | string ≤500 | null | | Optional second line under the item name. |
| unit | string ≤20 | null | | "hour", "kg", "piece"… |
| taxRate | number 0–100 | null | | Default VAT %; falls through to the invoice's taxRate. |
| active | boolean | | false archives — hidden from picker but kept for history. |
ItemResponse adds: id, active, lastUsedAt, createdAt, updatedAt (all ISO strings except id).
items.list() query
| Field | Type | Notes |
| ----------------- | -------------- | ------------------------------------------- |
| q | string | Substring filter on name. |
| includeInactive | boolean | Include soft-deleted entries. Default false. |
| limit | number 1–500 | Default 200. |
CRUD shape (same for all three)
| Method | HTTP | Endpoint |
| -------------------------------------------------------- | ------ | ------------------------- |
| si.profiles.list() / clients.list() / items.list() | GET | /api/{resource} |
| si.profiles.get(id) etc. | GET | /api/{resource}/:id |
| si.profiles.create(input) etc. | POST | /api/{resource} |
| si.profiles.update(id, patch) etc. | PATCH | /api/{resource}/:id |
| si.profiles.delete(id) etc. | DELETE | /api/{resource}/:id |
items.list({ q, includeInactive, limit }) accepts query params. items.touch(ids)
bumps lastUsedAt so the dashboard picker sorts them as "recently used".
Invoices: list, paginate, set payment
const page = await si.invoices.list({ tab: 'open', year: 2026, page: 1 });
for (const inv of page.invoices) console.log(inv.invoiceNr, inv.purchaserName);
await si.invoices.setPayment(invoiceId, { status: 'paid', paidAt: '2026-06-20' });
await si.invoices.deleteDraft(draftId);invoices.list() query
| Field | Type | Notes |
| ----------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| tab | all | open | paid | cancelled | draft | web | api | Default all. Tab counts come back in the response so you can paint badges. |
| q | string | Fuzzy match across invoice number + supplier + purchaser party names. |
| profileId | number | Filter by supplier profile id. |
| clientId | number | Filter by client id. |
| year | number | Calendar year of createdAt. |
| page | integer ≥1 | Default 1. |
| pageSize | integer 5–100 | Default 25. |
invoices.setPayment() body
| Field | Type | Notes |
| -------- | ----------------------------------- | --------------------------------------------------------------------------- |
| status | unpaid | paid | cancelled | Required. |
| paidAt | ISO date string | Only set with paid. Defaults to today server-side when omitted. |
| Method | HTTP | Endpoint |
| ------------------------------------------ | ------ | ------------------------------------- |
| si.invoices.generate(body) | POST | /api/invoices/generate |
| si.invoices.list(query) | GET | /api/invoices/mine |
| si.invoices.get(id) | GET | /api/invoices/mine/:id |
| si.invoices.setPayment(id, { status }) | PATCH | /api/invoices/mine/:id/payment |
| si.invoices.deleteDraft(id) | DELETE | /api/invoices/mine/:id |
Heads up:
list/get/setPaymentonly return invoices that were saved. UseinvoiceSettings.save: trueongenerateif you want them to show up here.
Auth — license key is the only credential you need
Every request carries your license key in the x-license-key header — the SDK
handles this automatically. The key resolves to your account; no JWT, no OAuth.
Heads up: don't ship the key in browser bundles or commit it to git. Rotate it any time from https://invoice.codurra.com/dashboard/developers; the old key is invalidated immediately.
Errors
The SDK throws SimpleInvoicingError for any non-2xx response.
import { SimpleInvoicingError } from 'simpleinvoicing';
try {
await si.invoices.generate({ /* … */ });
} catch (err) {
if (err instanceof SimpleInvoicingError) {
console.error(err.status, err.message, err.body);
} else throw err;
}| Status | Meaning |
| ------ | ----------------------------------------------------- |
| 400 | Validation error — body lists which fields failed. |
| 401 | Invalid or missing license key. |
| 402 | Monthly quota exhausted — upgrade for more headroom. |
| 404 | Resource not found (or not yours). |
| 429 | Rate limit hit — wait the Retry-After seconds. |
| 503 | Server saturated rendering invoices — retry shortly. |
| 5xx | Our side — retry with exponential backoff. |
Performance tips
TL;DR —
awaiteachinvoices.generate()call. Sequential is the path of least resistance and is what the API is tuned for.
The render endpoint applies two protective layers you should know about:
- Rate limit: 30 generate calls / minute per license key. Sequential awaits rarely brush this — each render takes a second or two and you'd need to be doing nothing else to exceed 30/min.
- Global concurrency cap (5): the server runs at most 5 invoice renders at
once across all customers. Beyond that, requests queue for up to 15
seconds, then start receiving
503 Server is busywith aRetry-Afterheader.
Don't Promise.all a batch
// ❌ Bad: fans out N renders at once. Trips the 503 as soon as the global
// cap fills up, and burns through your per-minute rate limit fast.
await Promise.all(invoices.map((i) => si.invoices.generate(i)));// ✅ Good: sequential. One in-flight render at a time.
for (const invoice of invoices) {
await si.invoices.generate(invoice);
}Need bounded parallelism for big batches?
If you really need to overlap a few at a time (e.g. rendering 1 000 invoices on
a CI job), cap concurrency to 3 — p-limit is the standard tool:
import pLimit from 'p-limit';
const limit = pLimit(3);
await Promise.all(invoices.map((i) => limit(() => si.invoices.generate(i))));That keeps you well under the global cap and avoids 503s entirely.
Retry on 429 / 503
The SDK throws SimpleInvoicingError; both codes carry a Retry-After header
on the underlying response. A small helper:
async function withRetry<T>(fn: () => Promise<T>, max = 3): Promise<T> {
for (let attempt = 0; ; attempt++) {
try { return await fn(); }
catch (err) {
if (!(err instanceof SimpleInvoicingError)) throw err;
const transient = err.status === 429 || err.status === 503;
if (!transient || attempt >= max) throw err;
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
}
}
const pdf = await withRetry(() => si.invoices.generate(input));Browser helpers
Importing from simpleinvoicing/browser gives you helpers for the PDF blob:
import { downloadPdf, printPdf, renderPdf } from 'simpleinvoicing/browser';
downloadPdf(pdf.invoice!, 'invoice.pdf');
printPdf(pdf.invoice!);
renderPdf(pdf.invoice!, 'preview-div');These touch document/window so they tree-shake out of Node bundles.
License
MIT
