# 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

1. Get an API key at https://oakstack.dev/dashboard/api-keys and set it as `OAKSTACK_API_KEY`.
2. Create a PDF where your app needs it:

```ts
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:

```sh
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:

```ts
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? }`. `color` is a hex color like `#1f6f4a` used for headings and accents. `logoUrl` is a public `https://` image URL or a `data:` URL.
- People and companies (`from`, `to`, `preparedFor`, `preparedBy`): `{ name, company?, addressLines?, email?, phone? }`.
- Dates are `YYYY-MM-DD` strings. Money is a number (`125.5`), with `currency` as a 3-letter code (default `USD`).
- 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 inline `data:` URLs. Anything else (localhost, private network addresses, `file:`) is blocked, and listed in the PDF's `blocked` field so you can see why an image is missing.
- **Paper:** `options.format` is `Letter` (default), `Legal`, `A4`, `A3`, `A5`, or `Tabloid`; `options.landscape: true` turns it. A CSS rule like `@page { size: A4; margin: 20mm }` in your HTML wins over the options.
- **Margins:** `options.margin` is 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: page` starts a new page; `break-inside: avoid` keeps a block (a table row, a signature area) together.
- **Backgrounds** print by default. Set `options.printBackground: false` to 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 needs `javascript: 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

```json
{
  "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"
}
```

- `url` is a private, signed download link that works for an hour. Get a fresh one any time with `oakstack.print.pdfs.get(id)`. Don't store the link; store the `id`, or the bytes.
- `fileExpiresAt` is 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 with `status: "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()`.
