PDFCraft
HTML to PDF API

HTML in,
PDF out.

One POST. Sub-second warm renders from a real Chromium, with the print options you actually need — margins, headers, footers, page ranges, wait conditions.

100 renders a month, free, no card. Then $9 for 2,000.

Try it right now — this key is live
curl -X POST https://api.pdfcraft.dev/v1/render \
  -H "Authorization: Bearer $PDFCRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>hello</h1>"}' \
  --output hello.pdf

Public on purpose, capped at 5 renders an hour per IP so you can try before signing up.

Why it is built this way

Warm Chromium, always

One browser per process, held open, handing out a fresh isolated context per render. A warm render is roughly ten times faster than a cold launch — which is what every serverless PDF service pays on every single request.

You are billed when the browser ran

Successes bill. A document that threw bills, because Chromium did the work. Timeouts, bad keys, malformed bodies and our own failures are free. The error reference marks every code, so the published rule and the charged rule are the same line of data.

Your documents are not stored

A synchronous render never touches disk — it exists in memory for the length of the render and then the context is destroyed. Output PDFs live behind a signed link for 24 hours, then a sweep job deletes them. Render records keep metadata only.

Quickstart
01

Run the curl

You now have a PDF on disk. That is the whole product — everything else is options.

02

Get your own key

Enter your email, click the link we send, copy the key off your dashboard. No password to choose.

Create an account
03

Install a client

TypeScript, Python and Go. Each has zero dependencies of its own and retries the failures worth retrying. Or keep using fetch — it is one endpoint.

npm install @pdfcraft-dev/pdf

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

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

const pdf = await pdfcraft.render({ html: "<h1>Invoice 1042</h1>" });
const { url } = await pdfcraft.renderToUrl({ url: "https://example.com" });

// The other direction.
const data = await pdfcraft.extractPdf(pdf);
console.log(data.tables[0]?.header);

All three are generated against the same contract the API validates requests with, so their types cannot drift from the server. Every language, with the trap each one sets.

The four endpoints
POST/v1/renderRender now. Returns application/pdf, or JSON with a signed URL when you send output: "url".
POST/v1/render/asyncQueue it and get a signed callback at your callback_url. Same body plus that one field.
GET/v1/renders/:idPoll one render.
POST/v1/extractThe other direction. A PDF in, its tables and labelled fields out as JSON, each with a bounding box.
GET/v1/usageRenders used, plan limit, when the period resets.
And the other direction: PDF to JSON

POST a PDF and get its tables and labelled fields back as JSON, each with a bounding box saying where on the page it came from. A table broken across fourteen pages comes back as one table.

No OCR and no model — extraction is geometric, so the same document always gives the same answer, and a scan is an honest 422 rather than a confident guess. Read the extraction docs.

curl -X POST https://api.pdfcraft.dev/v1/extract \
  -H "Authorization: Bearer $PDFCRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"file\": \"$(base64 -w0 invoice.pdf)\"}"

# { "fields": { "Invoice Number": {
#     "value": "INV-2026-0417", "page": 1,
#     "bbox": [115, 55, 176, 64] } },
#   "tables": [ … ], "usage": { "pages": 1 } }
And a third thing the same engine can do

How many of your PDFs are actually accessible?

Point it at a domain. It finds every PDF, checks each one against PDF/UA and WCAG 2.1 AA, and returns a prioritised report: what fails, how badly, what remediation would cost, and what to fix first.

We scanned 15 published federal documents on 2026-09-27. All 15 failed conformance and 3 have no tag structure at all — including the published text of the ADA Title II rule itself. Nobody’s portfolio is clean, which is why triage has to come before remediation.

Half of what we check is not machine-checkable by a conformance validator: whether the reading order stored in the file matches the page, whether a tagged table is really a grid, whether alt text describes anything or is just a filename.

First 25 documents free, no card. Next compliance deadline: 2027-04-26 (ADA Title II).

A scan of 15 federal documents

DocumentPagesTaggedBlockers
Economic Well-Being of U.S. Households82no2
H.R. 815 (enrolled)110no4
28 CFR §35.105 — the ADA Title II web acce1no4
Form 1040, U.S. Individual Income Tax Retu2yes2
Form 1099-MISC, Miscellaneous Information6yes2
Form 4506-T, Request for Transcript of Tax2yes0
Form 8822, Change of Address2yes3

Scanned with pdfa11y 0.0.11 plus our own geometric checks. All 15, with sources.

Rendering a page behind a login

Send a url instead of html, and pass the headers or cookies your own app would send. Wait for the selector that means the page is actually ready, rather than guessing with a delay.

curl -X POST https://api.pdfcraft.dev/v1/render \
  -H "Authorization: Bearer $PDFCRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/invoices/1042",
    "headers": { "Authorization": "Bearer YOUR_APP_TOKEN" },
    "cookies": [{ "name": "session", "value": "…" }],
    "options": { "waitFor": { "selector": "#ready", "networkIdle": true } },
    "output": "url"
  }'
Verifying an async callback

Your webhook secret is on the dashboard and is rotatable. It signs render callbacks only — it is not your billing webhook secret.

import { createHmac, timingSafeEqual } from "node:crypto";

const expected = createHmac("sha256", process.env.PDFCRAFT_WEBHOOK_SECRET!)
  .update(rawBody)
  .digest("hex");

const ok = timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(req.headers["x-signature"] as string),
);
Shape what ships next

Requested by developers

Every request below came from someone building on the API. Vote to move something up; I build from the top of this list.

Loading the board…

No account needed. Three submissions an hour.

Start with 100 renders a month.

No card, no trial clock, no sales call. When you outgrow it, $9 buys 2,000.