PDFCraft
TypeScript

HTML to PDF in TypeScript

One POST, a PDF back. No headless browser to install, no Chromium to keep patched, no fonts to install on the box — the render happens on a warm browser that is already running.

With the official client

There is a published TypeScript client, so the shortest version is two lines. It has no dependencies of its own, retries 429s and 5xxs for you, and turns the API’s error envelope into a typed error you can branch on. The raw HTTP below still works and is still supported — plenty of people would rather send a POST than take a dependency.

npm install @pdfcraft-dev/pdf
import { Renderer, type RenderInput } from '@pdfcraft-dev/pdf';

const pdfcraft = new Renderer(process.env.PDFCRAFT_API_KEY!);
const input: RenderInput = { html: '<h1>Invoice 1042</h1>' };
const pdf: Uint8Array = await pdfcraft.render(input);

On npm.

Render HTML to a PDF with plain HTTP

import { Renderer } from '@pdfcraft-dev/pdf';

const renderer = new Renderer(process.env.PDFCRAFT_API_KEY!);

// Buffer back
const pdf = await renderer.render({
  html: '<h1>Invoice 1042</h1>',
  options: { printBackground: true, margin: { top: '20mm' } },
});

// Or a signed URL, if you would rather not move the bytes
const { url, expires_at } = await renderer.renderToUrl({ url: 'https://example.com' });

Read a PDF back as JSON

The same key works for extraction. Send a PDF, name the fields you want by the label printed on the page, and get them back with the raw text, a coerced value, a confidence and a bounding box.

const result = await renderer.extract({
  file: pdfBuffer,
  schema: {
    account_number: 'string',
    closing_balance: 'currency',
    statement_date: 'date',
  },
  options: { rows_as_objects: true },
});

// Typed: value is string | number | boolean | null, and currency is present
// only for the fields you asked for as currency.
result.fields.closing_balance.value;
result.tables[0].header;          // string[] | null
result.tables[0].header_confidence; // 0-1

What bites in TypeScript

Errors are one shape

Every failure is {"error":{"code","message","docs_url"}} with a stable code, so you can switch on error.code rather than parsing prose. The two worth handling explicitly are rate_limited — honour Retry-After — and render_failed, which means your HTML broke rather than ours did.

The same thing in another language

Or try it with no code at all in the playground. A free key is 100 renders a month.