Oakprint: PDFs
Send HTML (or pick a hosted template and send data) and get back a PDF. Rendering uses real Chrome, so modern CSS works: flexbox, grid, web fonts, @page rules, page breaks. Every PDF is logged with its status, stored privately, and handed back as a download link.
Use it for invoices, receipts, proposals, reports, certificates, tickets, or any page of your app you want as a PDF.
Quick start
- Get an API key at https://oakstack.dev/dashboard/api-keys and set it as
OAKSTACK_API_KEY. - Create a PDF where your app needs it:
import { Oakstack } from "oakstack";
const oakstack = new Oakstack();
// From your own HTML:
const pdf = await oakstack.print.pdfs.create({
html: "<h1>Hello</h1><p>Rendered by Oakstack.</p>",
filename: "hello.pdf",
});
pdf.url; // private download link, valid for an hour
// Or from a hosted template:
const invoice = await oakstack.print.pdfs.create({
template: "invoice",
data: {
brand: { name: "Oak & Pine Renovations", color: "#1f6f4a" },
invoiceNumber: "INV-1042",
issueDate: "2026-10-04",
dueDate: "2026-11-03",
from: { name: "Oak & Pine Renovations", addressLines: ["12 Elm St", "Springfield"] },
to: { name: "Riley Chen", email: "riley@example.com" },
items: [
{ description: "Kitchen cabinet install", quantity: 1, unitPrice: 3200 },
{ description: "Hardware", quantity: 24, unitPrice: 12.5 },
],
taxRate: 8.25,
},
filename: "invoice-1042.pdf",
metadata: { orderId: "1042" },
});
const bytes = await oakstack.print.pdfs.download(invoice.id); // Uint8Array
create waits for the PDF (usually 1 to 5 seconds) and returns it. To send the PDF to a user, either redirect them to pdf.url, or download the bytes and attach or store them yourself.
Or with an agent: the MCP tools print_list_templates and print_create_pdf do the same. With curl:
curl https://oakstack.dev/api/v1/print/pdfs \
-H "Authorization: Bearer $OAKSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello</h1>","filename":"hello.pdf"}'
# Download it (follows a redirect to the file):
curl -L -o hello.pdf https://oakstack.dev/api/v1/print/pdfs/<id>/content \
-H "Authorization: Bearer $OAKSTACK_API_KEY"
Hosted templates
| Template | For |
|---|---|
invoice |
Itemized invoice with tax, discount, totals, payment instructions, and an optional PAID stamp. |
proposal |
Client proposal: summary, scope sections, pricing table, terms, signature lines. |
report |
Status or monthly report: headline metric cards, summary, sections with bullets and tables. |
Every template has a JSON Schema for its data and a complete working example:
const templates = await oakstack.print.templates.list(); // [{ id, name, description, schema, example }]
const { example } = await oakstack.print.templates.get("invoice");
await oakstack.print.pdfs.create({ template: "invoice", data: example }); // a full sample invoice
The fastest way to get the fields right is to start from example and change values. Data is checked strictly: a missing required field, a wrong type, or an unknown field (a typo like invoice_number for invoiceNumber) is rejected with a message naming it, and nothing is counted.
Shared fields:
brand:{ name, color?, logoUrl? }.coloris a hex color like#1f6f4aused for headings and accents.logoUrlis a publichttps://image URL or adata:URL.- People and companies (
from,to,preparedFor,preparedBy):{ name, company?, addressLines?, email?, phone? }. - Dates are
YYYY-MM-DDstrings. Money is a number (125.5), withcurrencyas a 3-letter code (defaultUSD). - Text with blank lines becomes separate paragraphs. All text is escaped, so data can't inject HTML.
Your own HTML
Send a full HTML document. Tips:
- Images and fonts: use public
https://URLs or inlinedata:URLs. Anything else (localhost, private network addresses,file:) is blocked, and listed in the PDF'sblockedfield so you can see why an image is missing. - Paper:
options.formatisLetter(default),Legal,A4,A3,A5, orTabloid;options.landscape: trueturns it. A CSS rule like@page { size: A4; margin: 20mm }in your HTML wins over the options. - Margins:
options.marginis one CSS length for every side ("0.5in", the default, or"12mm"), or{ top, right, bottom, left }. Use"0"for full-bleed designs. - Page breaks:
break-before: pagestarts a new page;break-inside: avoidkeeps a block (a table row, a signature area) together. - Backgrounds print by default. Set
options.printBackground: falseto drop them. - JavaScript is off by default. If the page draws charts or content with scripts, set
options.javascript: true. Scripts still can't reach private addresses. - Tailwind: include the compiled CSS in a
<style>tag (or link it from a public URL). The Tailwind Play CDN needsjavascript: true. - Rendering a page of your app: fetch its HTML yourself and send that. Oakstack can't log in to your app.
The PDF object
{
"id": "6f1c...",
"object": "print.pdf",
"status": "succeeded",
"source": "template",
"template": "invoice",
"filename": "invoice-1042.pdf",
"options": {
"format": "Letter",
"landscape": false,
"margin": { "top": "0.5in", "right": "0.5in", "bottom": "0.5in", "left": "0.5in" },
"printBackground": true,
"javascript": false
},
"metadata": { "orderId": "1042" },
"sizeBytes": 48211,
"pages": 1,
"durationMs": 1830,
"blocked": [],
"error": null,
"url": "https://...signed link...",
"urlExpiresAt": "2026-10-04T18:30:00.000Z",
"fileExpiresAt": "2026-10-11T17:30:00.000Z",
"createdAt": "2026-10-04T17:30:00.000Z",
"finishedAt": "2026-10-04T17:30:01.830Z"
}
urlis a private, signed download link that works for an hour. Get a fresh one any time withoakstack.print.pdfs.get(id). Don't store the link; store theid, or the bytes.fileExpiresAtis when Oakstack deletes the file: 1 day on Free, 7 days on Builder, 30 days on Studio. Oakstack is not long-term storage. If you need the PDF later (an invoice archive), download it and save it in your own storage. The log entry stays after the file is gone.oakstack.print.pdfs.delete(id)deletes the file right away (for sensitive documents). Needs a full-access key.
Errors
400 invalid_request: the request or the template data is wrong. The message names the field. Not counted.413 payload_too_large: HTML over 4 MB. Host big images at public URLs instead of inlining them. Not counted.422 invalid_request: the document itself failed to render: it took longer than 25 seconds, loaded too much (more than 100 resources or 20 MB), or produced a PDF over 20 MB. The message says which. This counts against your monthly limit, since it used the renderer. It's also logged withstatus: "failed".429 usage_limit_reached: the monthly PDF limit is used up. Upgrade, or wait for the 1st. Nothing was rendered.429 rate_limited: too many renders at once for your plan (Free 2, Builder 5, Studio 10). The SDK waits and retries automatically.503 internal_error: Oakstack's side failed (the renderer couldn't start, storage was down). Not counted, and the SDK retries automatically.
API reference
All endpoints take Authorization: Bearer <API key>.
| Method | Path | What it does |
|---|---|---|
POST |
/api/v1/print/pdfs |
Render a PDF. Body: html or template + data; filename, options, metadata optional. |
GET |
/api/v1/print/pdfs |
Recent PDFs, newest first. ?status=failed, ?limit= (max 100). |
GET |
/api/v1/print/pdfs/:id |
One PDF with a fresh download link. |
GET |
/api/v1/print/pdfs/:id/content |
Redirects to the file (curl -L). |
DELETE |
/api/v1/print/pdfs/:id |
Delete the file now. Full-access keys only. |
GET |
/api/v1/print/templates |
Templates with JSON Schema and example. |
GET |
/api/v1/print/templates/:id |
One template. |
POST accepts an Idempotency-Key header (the SDK sends one automatically), so a retried request returns the first PDF instead of rendering twice. metadata is up to 20 string values of your own, returned with the PDF.
Limits
| Plan | PDFs a month | At once | Files kept |
|---|---|---|---|
| Free | 25 | 2 | 1 day |
| Builder | 1,000 | 5 | 7 days |
| Studio | 5,000 | 10 | 30 days |
Per render: 25 seconds, 100 loaded resources, 20 MB of loaded resources, 4 MB of HTML, 20 MB of PDF. Limits reset on the 1st of each month (UTC). Check usage with oakstack.usage().