Quickstart
PageKiln is in private beta. Join the waitlist to get an API key — until then you can read along; every example below runs unchanged once you have a key.
All requests authenticate with a bearer token:
export PAGEKILN_API_KEY="pk_live_..."1. Create a template
Section titled “1. Create a template”Templates are Typst documents. Each top-level key
of the request’s data is available in sys.inputs as a typed value. Save
this as invoice.typ:
#import sys: inputs= Invoice #inputs.invoice_noAmount due: #inputs.amount EURPOST /v1/templates validates the template by rendering it once, so pass
validation_data with sample inputs — without it, a template that references
sys.inputs is rejected with a 422.
curl -X POST https://api.pagekiln.dev/v1/templates \ -H "authorization: Bearer $PAGEKILN_API_KEY" \ -H "content-type: application/json" \ -d "{\"name\": \"invoice\", \"source\": $(jq -Rs . < invoice.typ), \"validation_data\": {\"invoice_no\": \"INV-000\", \"amount\": 0}}"The jq call JSON-encodes invoice.typ into the request body.
The JS SDK ships with the private beta.
import { readFile } from "node:fs/promises";import { PagekilnClient } from "@pagekiln/sdk";
const pagekiln = new PagekilnClient({ apiKey: process.env.PAGEKILN_API_KEY! });
const template = await pagekiln.templates.create({ name: "invoice", source: await readFile("invoice.typ", "utf8"), assets: [], validation_data: { invoice_no: "INV-000", amount: 0 }});// → { template_id: "tpl_...", version: 1, ... }Save {"invoice_no": "INV-000", "amount": 0} as validation.json, then:
pagekiln templates create --name invoice --source invoice.typ --data validation.json2. Render a PDF
Section titled “2. Render a PDF”curl -X POST https://api.pagekiln.dev/v1/render \ -H "authorization: Bearer $PAGEKILN_API_KEY" \ -H "content-type: application/json" \ -d '{"template_id": "tpl_...", "data": {"invoice_no": "INV-001", "amount": 120}, "output": "url"}'# → {"url": "https://...", "pages": 1, ...}const render = await pagekiln.renderUrl({ template_id: template.template_id, data: { invoice_no: "INV-001", amount: 120 }});console.log(render.url); // signed URL to the PDFpagekiln render --template tpl_... --data data.json --out invoice.pdf3. Run a batch
Section titled “3. Run a batch”One request, many documents — with status tracking, partial-failure handling and signed URLs per item:
curl -X POST https://api.pagekiln.dev/v1/batches \ -H "authorization: Bearer $PAGEKILN_API_KEY" \ -H "content-type: application/json" \ -d '{"template_id": "tpl_...", "items": [{"data": {"invoice_no": "INV-001", "amount": 120}}, {"data": {"invoice_no": "INV-002", "amount": 80}}]}'# → {"batch_id": "job_...", "total": 2, "status": "queued"}
curl https://api.pagekiln.dev/v1/batches/job_... \ -H "authorization: Bearer $PAGEKILN_API_KEY"# → {"status": "completed", "counts": {"total": 2, "done": 2, "failed": 0}, ...}const batch = await pagekiln.batches.create({ template_id: template.template_id, items: [ { data: { invoice_no: "INV-001", amount: 120 } }, { data: { invoice_no: "INV-002", amount: 80 } } ]});
const status = await pagekiln.batches.get(batch.batch_id);pagekiln batches create --template tpl_... --items items.json --waitTo get notified instead of polling, register a webhook for batch.completed
— see the API Reference.