Someone on your team asks for a "Download as PDF" button on the invoice page. You look at the options: puppeteer adds a ~300MB Chromium download to your deploy, wkhtmltopdf is abandoned, and the client-side libraries choke on your CSS. All you want is the page you already built, as a PDF.
SnapPDF does that one thing. You give it a URL, it renders the page in a real headless browser, and it returns the PDF bytes. No API key, no JSON envelope, no SDK.
The whole API
GET https://snappdf.dedyn.io/v1/pdf?url=https://EXAMPLE.com
Try it from your terminal:
curl -o example.pdf "https://snappdf.dedyn.io/v1/pdf?url=https://example.com"
Open example.pdf. That's the entire integration.
The response is the raw PDF (Content-Type: application/pdf), not JSON with a base64 blob or a link you have to fetch in a second round trip. Whatever bytes come back, you can write straight to disk or stream straight to a browser.
A few query params when the defaults don't fit:
| Param | What it does |
|---|---|
format |
Paper size, A4 by default |
landscape |
true or false
|
background |
true or false, render CSS backgrounds |
scale |
Zoom the render, e.g. 0.8 to fit wide tables |
wait_for_selector |
CSS selector to wait for before capture |
wait_for_selector is the one that saves you on client-rendered apps. If your invoice page is React and the line items arrive after an API call, pass wait_for_selector=.invoice-total and the capture waits until that element exists instead of snapshotting a spinner.
The download button
Here's the pattern I actually use: an Express route that proxies the PDF, so the browser gets a file download instead of navigating away. Node 18+ has fetch built in, so there are zero dependencies.
app.get("/invoices/:id/pdf", async (req, res) => {
const pageUrl = `https://yourapp.com/invoices/${req.params.id}`;
const upstream = await fetch(
"https://snappdf.dedyn.io/v1/pdf?" +
new URLSearchParams({
url: pageUrl,
wait_for_selector: ".invoice-total",
background: "true",
})
);
if (!upstream.ok) {
return res.status(502).send("PDF render failed");
}
res.set("Content-Type", "application/pdf");
res.set(
"Content-Disposition",
`attachment; filename="invoice-${req.params.id}.pdf"`
);
res.send(Buffer.from(await upstream.arrayBuffer()));
});
The frontend part is an anchor tag:
<a href="/invoices/1042/pdf">Download as PDF</a>
The Content-Disposition: attachment header is what turns a navigation into a download. Skip it and the PDF opens in a tab, which is sometimes what you want for a "Print view" link instead.
Two things worth knowing before you ship this:
-
The renderer visits the URL like an anonymous browser. If your invoice page sits behind a login, the renderer sees your login screen. Expose the printable view on a tokenized URL (
/invoices/1042/print?token=...) that your proxy route generates. - Renders take a couple of seconds since a real browser loads your page. For a button click that's fine. If you're generating hundreds of monthly statements, run it in a queue and cache the output.
Beyond invoices, the same proxy route handles receipts, shipping labels, certificates, and reports. Anything you can build as an HTML page, you can hand to users as a PDF, and HTML plus CSS is a far better layout engine than any PDF library's rectangle-drawing API.
Examples in more languages live in the repo: github.com/clause-netizen/snappdf-api.
Top comments (0)