Stop babysitting a headless browser just to make a PDF
Send HTML, get a production-ready PDF back. One API call. No Chromium in your Dockerfile, no memory leaks at 3am, no fonts silently falling back to Times New Roman.
No credit card required.
You already know how this goes
Generating a PDF looks like a two-hour task until you put it in production. Then it becomes a permanent tenant in your backlog.
Doing it yourself
Puppeteer works on your laptop and dies in CI because libnss3 is missing. Chromium eats 150 MB per instance and OOM-kills your container under load. It won't run on Vercel, Netlify or Lambda at all. Your brand font renders as a box. Six months later, someone has to upgrade all of it.
Using Papyra
One HTTP call. We keep the browser fleet, the fonts, the memory limits and the security sandbox running. Your app stays a normal app that makes a normal HTTP request — the kind that works on any host, including serverless.
Integrate in about five minutes
Add ?wait=true and the request holds until the PDF is ready, so you get a URL back in the same call. Drop it and you get a job id to poll instead — useful for big documents.
curl -X POST 'https://papyra.me/v1/pdf?wait=true' \
-H 'Authorization: Bearer pk_live_...' \
-H 'Content-Type: application/json' \
-d '{
"html": "<h1>Invoice #1042</h1><p>Total: $250.00</p>",
"filename": "invoice-1042"
}'const res = await fetch('https://papyra.me/v1/pdf?wait=true', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAPYRA_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: '<h1>Invoice #1042</h1><p>Total: $250.00</p>',
filename: 'invoice-1042',
}),
});
const { url } = await res.json();
// -> https://papyra.me/d/3f0c... (signed, expires in 24h)import os, requests
res = requests.post(
"https://papyra.me/v1/pdf?wait=true",
headers={"Authorization": f"Bearer {os.environ['PAPYRA_KEY']}"},
json={
"html": "<h1>Invoice #1042</h1><p>Total: $250.00</p>",
"filename": "invoice-1042",
},
)
print(res.json()["url"])<?php
$res = file_get_contents('https://papyra.me/v1/pdf?wait=true', false,
stream_context_create(['http' => [
'method' => 'POST',
'header' => "Authorization: Bearer {$_ENV['PAPYRA_KEY']}\r\n" .
"Content-Type: application/json\r\n",
'content' => json_encode([
'html' => '<h1>Invoice #1042</h1><p>Total: $250.00</p>',
'filename' => 'invoice-1042',
]),
]]));
echo json_decode($res, true)['url'];Three ways to send content
Raw HTML
Send the markup and CSS you already have. Best when your app builds the document itself.
A public URL
Point us at a page and we render it. Good for turning an existing report page into a downloadable file.
A saved template
Store the layout once, then send only JSON data. Keeps your invoice markup out of your application code.
What you get out of the box
Real Chrome rendering
Flexbox, grid, web fonts, page breaks, headers and footers. If Chrome prints it right, so do we.
Signed, expiring links
Every document gets a private URL signed with HMAC. No public buckets, no guessable IDs.
Isolation per document
Fresh browser context per render, network-locked and privilege-stripped. Your HTML never sees anyone else's.
Predictable quotas
A hard monthly limit and a per-minute rate cap. No surprise bill because a loop went wrong.
Sync or async
Wait for the file in the same request, or queue it and poll. Paid plans get queue priority.
Honest error messages
Stable error codes and messages that say what to fix, instead of a stack trace or a bare 500.
Simple pricing
Start free. Upgrade when the free tier stops fitting — not before.
Free
$0
100 documents/month
Starter
$29 /mo
5,000 documents/month
Pro
$99 /mo
50,000 documents/month
Questions people actually ask
How is this different from running Puppeteer myself?
Mechanically, it isn't — we run Chromium too. The difference is who maintains it. You skip the Dockerfile with forty system packages, the OOM tuning, the font installation, the zombie-process reaping, the security sandboxing of third-party HTML, and every Chrome upgrade from now on. If you enjoy that work, self-hosting is genuinely a fine choice.
Can I use my own fonts and CSS?
Yes. Web fonts load over HTTPS like they would in a browser, and we wait for document.fonts.ready before printing, so your typeface doesn't silently fall back. Full CSS support, including @page rules for margins and page size.
How long are documents stored?
Seven days by default, then they are deleted permanently. The download link is signed and expires independently. If you need the file long-term, download it and store it on your side — we are a renderer, not an archive.
What happens if I hit my monthly limit?
The API returns 429 quota_exceeded with your current usage, and rendering stops until the next cycle or an upgrade. We never silently overcharge you for going over.
Is my content private?
Each render runs in a throwaway browser context with no shared cookies, cache or storage, inside a container with no route to our database. Files land in a private bucket and are only reachable through a signed, expiring URL scoped to that one document.
Can I cancel anytime?
Yes, in one click from your billing page — it opens Stripe's portal directly. No email, no retention call. You keep access until the end of the period you already paid for.
Generate your first PDF in five minutes
100 documents a month, free forever, no credit card.
Create your free account