TypeScript and JavaScript SDK
npm install quirepdf
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
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
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:
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
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
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
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:
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 for every type.