composepdf
v0.1.0
Published
Typed client for the Compose PDF API — render server-side templates to PDF, with the payload types generated from each template's own data contract.
Maintainers
Readme
composepdf
Typed client for the Compose PDF API.
Design the template in the browser. Send data. Get a print-ready PDF back — and
get the payload's TypeScript types from the template itself, so the shape you
send is checked at compile time rather than discovered from a 400.
npm i composepdfNo dependencies. Works in Node 18+, Bun, Deno, Cloudflare Workers and any
browser-like runtime with fetch.
Render
import { writeFile } from 'node:fs/promises';
import { createClient } from 'composepdf';
const composepdf = createClient({ apiKey: process.env.COMPOSEPDF_API_KEY! });
const { bytes } = await composepdf.render('cnv_…', {
data: {
customer: { name: 'Acme Inc.' },
items: [
{ name: 'Design work', price: 1200 },
{ name: 'Hosting', price: 90 },
],
},
});
await writeFile('invoice.pdf', bytes);The template id comes from composepdf.listTemplates() or from the studio.
Rendering always uses the published version, never a work-in-progress draft.
Types from your templates
Every published template declares what data it reads, and the API serves that
declaration. composepdf types turns it into one file:
npx composepdf types --out src/composepdf-types.tsimport { createClient } from 'composepdf';
import type { Templates } from './composepdf-types';
const composepdf = createClient<Templates>({ apiKey: process.env.COMPOSEPDF_API_KEY! });
await composepdf.render('cnv_…', { data: { customer: { name: 'Acme Inc.' } } });
// ^ only your template ids ^ checked against that templateNothing is hand-maintained: the interface is the template's own data contract, so republishing a template with a new field and regenerating is the whole migration. Commit the generated file — it has no timestamp, so an unchanged workspace regenerates to no diff.
The command reads COMPOSEPDF_API_KEY from the environment (--key overrides),
and writes composepdf-types.ts unless given --out.
Errors say which path was wrong
A payload the template cannot accept is rejected before the render starts, naming every offending path:
import { ComposePdfError } from 'composepdf';
try {
await composepdf.render('cnv_…', { data });
} catch (e) {
if (e instanceof ComposePdfError && e.code === 'data_contract_violation') {
for (const issue of e.issues) {
console.error(issue.path, issue.message); // items[3].price expected number
}
}
}ComposePdfError also carries status, requestId (quote it to support) and
retryAfter for 429/503.
Many records, one file
const { bytes, records } = await composepdf.render('cnv_…', {
dataList: [invoiceA, invoiceB, invoiceC],
});One document per record, collated into a single PDF in order. data and
dataList are mutually exclusive.
Long renders
const job = await composepdf.renderAsync('cnv_…', { dataList: manyRecords });
let render = await composepdf.getRender(job.id);
while (!render.done) {
await new Promise((r) => setTimeout(r, 1000));
render = await composepdf.getRender(job.id);
}
const bytes = await composepdf.downloadRender(job.id);An accepted job always keeps its bytes. A synchronous render keeps them only
with options: { store: true }.
Retries that cannot double-charge
await composepdf.render('cnv_…', { data }, { idempotencyKey: `order-${orderId}` });Presenting the same key again replays the stored PDF without rendering or billing a second time.
API
| | |
|---|---|
| listTemplates() | Published templates this key can render |
| render(id, request?, options?) | The PDF, as bytes |
| renderAsync(id, request?, options?) | A job id, for renders past the synchronous budget |
| getSchema(id, options?) | JSON Schema, the paths the template reads, and a sample payload |
| getSchemaSource(id, 'ts' \| 'zod', options?) | The same contract as source |
| listRenders(params?) / getRender(id) / downloadRender(id) | Render history and stored PDFs |
| getUsage() | This month's usage — read it to stop before a 402 |
| health() | Open route; answers without a key |
The surface is exactly the published OpenAPI document
(GET /v1/openapi.json). Full reference: composepdf.com/docs.
Keep the key server-side
An API key can render and download every document in its scope. Do not ship it to a browser. If you need a live preview inside your own product, use the embedded preview — it takes a scoped embed token instead, and renders in the visitor's browser.
License
MIT
