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

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,

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 simpleinvoicing
import { 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 historylist / 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/browser for 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 hideBranding and 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.pdf

invoices.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.licenseKey is filled in by the SDK from createClient({ 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 / setPayment only return invoices that were saved. Use invoiceSettings.save: true on generate if 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 — await each invoices.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 busy with a Retry-After header.

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