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.
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.pdfPublic on purpose, capped at 5 renders an hour per IP so you can try before signing up.
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.
Run the curl
You now have a PDF on disk. That is the whole product — everything else is options.
Get your own key
Enter your email, click the link we send, copy the key off your dashboard. No password to choose.
Create an accountInstall 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);pip install pdfcraft
from pdfcraft import PDFCraft
client = PDFCraft(os.environ["PDFCRAFT_API_KEY"])
pdf = client.render(html="<h1>Invoice 1042</h1>")
open("invoice.pdf", "wb").write(pdf)
# The other direction.
data = client.extract_pdf(pdf)
print(data["tables"][0]["header"])go get github.com/igaurav-dev/pdfcraft-go
client, err := pdfcraft.New(os.Getenv("PDFCRAFT_API_KEY"))
if err != nil {
log.Fatal(err)
}
pdf, err := client.Render(ctx, pdfcraft.RenderRequest{
HTML: "<h1>Invoice 1042</h1>",
})
os.WriteFile("invoice.pdf", pdf, 0o644)
// The other direction.
data, err := client.ExtractPDF(ctx, pdf, pdfcraft.ExtractRequest{})no dependency at all
curl -X POST https://api.pdfcraft.dev/v1/render \
-H "Authorization: Bearer $PDFCRAFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Invoice 1042</h1>"}' \
--output invoice.pdfAll 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.
/v1/renderRender now. Returns application/pdf, or JSON with a signed URL when you send output: "url"./v1/render/asyncQueue it and get a signed callback at your callback_url. Same body plus that one field./v1/renders/:idPoll one render./v1/extractThe other direction. A PDF in, its tables and labelled fields out as JSON, each with a bounding box./v1/usageRenders used, plan limit, when the period resets.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 } }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
| Document | Pages | Tagged | Blockers |
|---|---|---|---|
| Economic Well-Being of U.S. Households | 82 | no | 2 |
| H.R. 815 (enrolled) | 110 | no | 4 |
| 28 CFR §35.105 — the ADA Title II web acce | 1 | no | 4 |
| Form 1040, U.S. Individual Income Tax Retu | 2 | yes | 2 |
| Form 1099-MISC, Miscellaneous Information | 6 | yes | 2 |
| Form 4506-T, Request for Transcript of Tax | 2 | yes | 0 |
| Form 8822, Change of Address | 2 | yes | 3 |
Scanned with pdfa11y 0.0.11 plus our own geometric checks. All 15, with sources.
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"
}'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),
);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.
Start with 100 renders a month.
No card, no trial clock, no sales call. When you outgrow it, $9 buys 2,000.