# TypeScript and JavaScript SDK

> Render PDFs from Node.js, Bun, Deno or edge runtimes with the quirepdf package, with typed template data and a single QuireError class.

```bash
npm install quirepdf
```

```ts
import { Quire } from "quirepdf";
import { writeFile } from "node:fs/promises";

const quire = new Quire(); // reads QUIRE_API_KEY

const pdf = await quire.render({
  order_id: "ORD-88213",
  customer: { name: "Lena Fischer", email: "lena@example.de" },
  lines: [{ sku: "TS-BLK-M", name: "Organic tee", qty: 2, price: 29 }],
  currency: "€",
});
await writeFile("order.pdf", pdf.bytes);
console.log(pdf.template, pdf.templateSource, pdf.pages); // "document" "fallback" 1
```

The package has no runtime dependencies. It uses the global `fetch`, so it runs in Node.js 18+, Bun, Deno, edge runtimes and browsers. Keep your API key on the server, though: anything shipped to a browser is public.

## Configuration

```ts
const quire = new Quire({
  apiKey: process.env.QUIRE_API_KEY, // default: env QUIRE_API_KEY
  baseUrl: "https://api.quirepdf.dev", // default: env QUIRE_API_URL, then this
  timeoutMs: 10_000, // default 30 s
});
```

You can also pass `fetch` to use a custom implementation, for example in tests.

## Render

```ts
const result = await quire.render(data, { template: "invoice", format: "pdf" });
```

| Option | Type | Notes |
|---|---|---|
| `template` | string | Omit to auto-detect or use the generic layout |
| `format` | `"pdf"` \| `"png"` | Default `"pdf"` |
| `page` | number | 1-based page for PNG |
| `test` | boolean | Free watermarked test render, not counted (from 0.2.0) |
| `signal` | `AbortSignal` | Cancel the request |

`render` resolves to a `RenderResult`:

| Field | Type | Meaning |
|---|---|---|
| `bytes` | `Uint8Array` | The PDF or PNG file |
| `contentType` | string | `application/pdf` or `image/png` |
| `pages` | number | Pages in the whole document |
| `template` | string | The template used |
| `templateSource` | `"explicit"` \| `"detected"` \| `"fallback"` | How it was chosen |
| `hint` | string, optional | A template that almost matched, e.g. `invoice (seller is required)` |
| `renderMs` | number | Server render time |
| `test` | boolean | `true` for watermarked test renders |
| `quota` | `{ limit, remaining }`, optional | Your monthly quota after this render (absent for test renders) |

To return the file from a route handler, pass a copy to `Response`: `new Response(pdf.bytes.slice(), { headers: { "Content-Type": "application/pdf" } })`. Recent TypeScript versions reject the `Uint8Array` itself as a response body; `.slice()` gives it the type `Response` expects.

## Typed template data

Name a gallery template and the data is checked against that template's type at compile time:

```ts
import { Quire, type InvoiceData } from "quirepdf";

const quire = new Quire();

const invoice: InvoiceData = {
  number: "INV-2026-0142",
  issued: "1 Oct 2026",
  due: "15 Oct 2026",
  seller: { name: "Northwind Studio", address: ["221 Market Street", "San Francisco, CA 94105"] },
  customer: { name: "Ravi Kumar", address: ["14 Residency Road", "Bengaluru 560025"] },
  items: [{ description: "Pro plan", qty: 1, unit_price: 49 }],
  tax: { label: "GST (18%)", rate: 0.18 },
};

const pdf = await quire.render(invoice, { template: "invoice" });

// Compile errors:
// quire.render({ number: "INV-1" }, { template: "invoice" });          missing issued, due, seller…
// quire.render({ ...invoice, status: "late" }, { template: "invoice" }); not a valid status
```

The types are generated from the templates' JSON Schemas: `InvoiceData`, `ReceiptData`, `QuoteData`, `CreditNoteData`, `StatementData`, `CertificateData` and `DocumentData`. Field descriptions from the schema appear in your editor's tooltips. Without `template`, `render` accepts any object.

## Preview a page as PNG

```ts
const first = await quire.render(invoice, { template: "invoice", format: "png" });
await writeFile("invoice-p1.png", first.bytes);
for (let page = 2; page <= first.pages; page++) {
  const png = await quire.render(invoice, { template: "invoice", format: "png", page });
  await writeFile(`invoice-p${page}.png`, png.bytes);
}
```

Each PNG call renders one page and counts as one render.

## Validate for free

```ts
const check = await quire.validate(invoice, { template: "invoice" });
if (!check.valid) console.log(check.errors); // [{ path: "seller", message: "seller is required" }]
```

`validate` returns `{ valid, template, source, hint?, errors }` without rendering and doesn't count against your quota. Invalid data is a result here, not an exception. Leave out `template` to see which template your data would get.

## Templates, usage and keys

```ts
const templates = await quire.templates.list(); // [{ name, title, description, category, tags, version }]
const { schema, sample } = await quire.templates.get("receipt");

const usage = await quire.usage(); // { email, plan, period: "2026-10", used, limit, remaining }

const keys = await quire.keys.list(); // [{ id, prefix, name, createdAt, lastUsedAt, current }]
const { key, apiKey } = await quire.keys.create("ci"); // apiKey is shown only now
await quire.keys.revoke(key.id);
```

`createdAt` and `lastUsedAt` are `Date` objects. `current` is `true` for the key this client is using.

## Errors

Every failure throws a `QuireError`:

```ts
import { Quire, QuireError } from "quirepdf";

try {
  await quire.render(data, { template: "invoice" });
} catch (err) {
  if (err instanceof QuireError) {
    console.error(err.status, err.type); // 422 "invalid_data"
    for (const f of err.fields) console.error(f.path, f.message); // "items[0].qty" "items[0].qty must be a number"
  }
  throw err;
}
```

| Property | Meaning |
|---|---|
| `status` | HTTP status, or `0` for client-side failures |
| `type` | The API error type (`invalid_data`, `unauthorized`, `quota_exceeded`, …), or `network`, `timeout`, `aborted` |
| `summary` | The API's message on its own |
| `message` | `summary` plus one line per field problem, so logging the error shows everything |
| `fields` | Every `{ path, message }` problem, for `invalid_data` |

The SDK doesn't retry. See [Errors](/docs/errors) for every type.