You have a page that renders fine in a browser and now someone wants it as a file: an invoice to attach, a report to archive, a pricing page to snapshot before the next deploy. Wiring up headless Chrome for that means a Puppeteer install, a container with the right shared libraries, and a cold-start problem in CI. For one GET request's worth of work, that stack is overkill.
SnapPDF does the browser part server-side. You send it a URL, it sends back a PDF.
The one-liner
No API key, no headers, no JSON envelope. GET /v1/pdf with a url param returns raw PDF bytes:
curl -G "https://snappdf.dedyn.io/v1/pdf" \
--data-urlencode "url=https://example.com" \
-o page.pdf
Use -G with --data-urlencode instead of pasting the target URL into the query string yourself. If the target has its own query params (?id=42&tab=summary), a hand-built URL splits at the first & and you'll render the wrong page. --data-urlencode escapes it for you.
Optional query params, all of which map to what you'd set in a print dialog or a Puppeteer config:
-
format— paper size,A4by default -
landscape—trueorfalse -
background—trueorfalse, controls whether CSS backgrounds print -
scale— a number, for shrinking wide layouts onto the page -
wait_for_selector— a CSS selector to wait for before rendering
That last one matters most in practice. If the page draws a chart or loads data client-side, render timing decides whether your PDF shows content or a spinner. Point wait_for_selector at an element that only exists once the page is ready:
curl -G "https://snappdf.dedyn.io/v1/pdf" \
--data-urlencode "url=https://example.com/report?id=42" \
--data-urlencode "wait_for_selector=#chart-loaded" \
--data-urlencode "background=true" \
-o report.pdf
The same thing from Node
Global fetch handles it without any dependencies. Build the URL with URL so encoding stays correct, then write the bytes to disk:
import fs from "node:fs/promises";
const api = new URL("https://snappdf.dedyn.io/v1/pdf");
api.searchParams.set("url", "https://example.com/invoice/42");
api.searchParams.set("wait_for_selector", "#invoice-total");
const res = await fetch(api);
if (!res.ok) throw new Error(`SnapPDF returned ${res.status}`);
const pdf = Buffer.from(await res.arrayBuffer());
await fs.writeFile("invoice.pdf", pdf);
What comes back
The response body is the PDF itself, Content-Type: application/pdf. There is no JSON wrapper, no base64 field to decode, no second request to fetch the result. Pipe it to a file, stream it to S3, or set it as an email attachment as-is. If you want a sanity check in a script, the first bytes of any valid response are %PDF.
That shape is what makes it useful in CI. A GitHub Actions step that archives your live pricing page on every release needs nothing but curl:
- name: Snapshot pricing page
run: |
curl -G "https://snappdf.dedyn.io/v1/pdf" \
--data-urlencode "url=https://yoursite.com/pricing" \
--fail -o pricing-$(date +%F).pdf
- uses: actions/upload-artifact@v4
with:
name: pricing-snapshot
path: pricing-*.pdf
--fail makes curl exit nonzero on an error status, so a bad render fails the job instead of uploading an HTML error page with a .pdf extension. I run a variant of this that snapshots terms-of-service and pricing pages on a schedule, which turns "what did the page say in March" from an argument into a folder of dated PDFs.
Try it
Examples for other languages and CI setups live in the repo: github.com/clause-netizen/snappdf-api.
Top comments (0)