Use case
URL to PDF
Pass an address and get the page back as a file. Chromium opens it as a browser would, runs its JavaScript, waits until it has loaded, and prints it with the page’s own print stylesheet or captures it as an image in the window size you set. The page has to be public: what needs a login is sent as HTML instead.
Free plan with 100 units a month, no credit card
PDF or screenshot
Without output the answer is a PDF. png, jpg and webp capture the window instead, the whole length of the page with image.fullPage or one element with image.clipSelector.
The page’s own print CSS
A PDF is printed, not photographed: the @media print rules of the page apply, backgrounds are kept, and settings choose the paper, the margins and a header and a footer with page numbers.
Pages that load late
The page is captured when it has loaded, the network is quiet and the fonts are ready. waitFor.selector waits for an element your script adds, and waitFor.delayMs adds a fixed pause.
A fresh browser every time
Each render starts without cookies or storage and opens the address anew. The same address twice is two renders, and each shows the page as it is at that moment.
Guard rails
Only public addresses over HTTPS. Private, loopback and internal addresses are refused, for the page and for everything it loads, and a page that answers with an HTTP error fails the render instead of being printed.
Rendered in the EU
Chromium runs in Frankfurt: that is where the request to the page comes from, and the file is stored in the EU.
Print a web page with one request
curl -X POST "https://api-eu.formfeed.dev/v1/renders?output=binary" \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/", "settings": {"paper": {"format": "A4"}, "margin": {"top": "15mm", "bottom": "15mm"}}}' \
-o page.pdfFull request and response shapes in the Docs.
What the page has to be
The address has to answer over HTTPS from the public internet. Private, loopback and internal addresses are refused, and nothing the page loads, an image, a script, a font, may come from such an address either, wherever a redirect leads. A page that answers with an HTTP error or cannot be reached fails the render with navigation_failed, and a failed render is not counted.
The request carries no cookies and no headers of yours, so a page behind a login shows the renderer its login form. For such a page, build the HTML where the session is and send it as html: HTML to PDF. A site that turns automated browsers away answers the renderer with its challenge, too.
Paper, margins and the page’s print stylesheet
A PDF is the page as its print stylesheet lays it out, as the print dialog of a browser would show it. Paper, orientation and margins come from settings, A4 upright unless you say otherwise, a header and a footer can repeat on every page, and with preferCssPageSize an @page rule of the page decides the size instead.
{
"url": "https://example.com/reports/2026-09",
"settings": {
"paper": { "format": "A4", "landscape": true },
"margin": { "top": "15mm", "right": "12mm", "bottom": "18mm", "left": "12mm" },
"footer": {
"height": "10mm",
"html": "<div style=\"width:100%;text-align:center;font-size:8px\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>"
}
}
}For printing, the page is as wide as the paper less its margins, and A4 upright is 794 CSS pixels across, so a responsive page prints the layout it has for a narrow window. Landscape or a larger paper gives it more room, scale shrinks or enlarges the content between 0.1 and 2, and pageRanges keeps only the pages you name. Header and footer are printed outside the page and carry their own inline styles.
A screenshot instead of a PDF
With "output": "png", "jpg" or "webp" the page is not printed but captured as the screen shows it, with its screen styles. settings.image sets the window: width and height in CSS pixels, 1200 × 630 unless you say otherwise, and deviceScaleFactor, which is 2 by default and doubles the pixels of the file. fullPage captures the whole length of the page, clipSelector one element, and quality sets the compression of JPG and WebP.
{
"url": "https://example.com/reports/2026-09",
"output": "png",
"settings": {
"image": { "width": 1440, "height": 900, "deviceScaleFactor": 1, "fullPage": true }
}
}For the preview image of a page of your own, a template is the better source than a screenshot: Open Graph image API.
Pages that finish late
A page that builds itself with JavaScript is captured when it is done, not when its first byte arrives: the renderer waits for the load event, for the network to go quiet and for the fonts. Where that is not enough, waitFor.selector waits for an element, five seconds unless waitFor.timeoutMs says otherwise, and waitFor.delayMs adds a pause of up to ten seconds. A page that keeps a connection open never goes quiet: waitFor.networkIdle: false captures it right after the load event.
A page has 30 seconds to load. A request nobody should wait for goes with "mode": "async" and reports through a webhook, and a list of addresses is a batch: up to 1,000 pages in one request.
Every request loads the page again
A live render from a template is answered from the stored file when the identical request comes again within 24 hours. A URL is different: the page behind it may have changed, so a URL render is never answered from an earlier one. Every request opens the page, shows it as it is at that moment and counts as a render. To make a retry safe, send an Idempotency-Key: once the first request has been answered, the same key returns that answer for 24 hours, and nothing is rendered twice.
Each render starts in a fresh browser without cookies or storage, with the language en-GB and the time zone Europe/Berlin unless settings.locale and settings.timezone name others. A page that greets a first visit with a consent banner shows it in the capture; a PDF leaves it out only where the page’s print stylesheet hides it, and image.clipSelector captures one element instead of the window. The page, its stylesheets and its scripts are loaded anew each time; its pictures and fonts may come from the renderer’s cache, which keeps a file under its address for up to a week.
When the page is yours
Printing an address is the short way for a page that exists anyway. When the page is yours and exists only to become a PDF, a receipt, a report, a ticket, the layout is better kept as a template: the request carries the data instead of an address, nothing has to be reachable from the internet, an identical live request within 24 hours costs nothing the second time, and the editor shows the pages before anything is sent. HTML that your server renders anyway can go as it is, as html.
What a page costs
A PDF of up to five pages is one unit, and every further five pages add one; a screenshot is one unit. The Free plan has 100 units a month and asks for no credit card, and its PDFs may have up to 20 pages. Starter has 3,000 units for €19 a month and prints up to 200 pages. Renders with a test key cost nothing, and a render that fails is never counted. Every plan is on the pricing page.
FAQ
- Can it render pages behind a login?
- No. The request carries no cookies and no headers of yours, so the renderer sees what a visitor without an account sees. Render such a page where the session is and send the result as
html. - Are pages rendered from the EU?
- Yes. Chromium runs in Frankfurt, and the file is stored in the EU.
- Why does the PDF look different from the page in my browser?
- A PDF uses the print stylesheet of the page and the width of the paper, as the print dialog of a browser does. For the page as the screen shows it, ask for
png,jpgorwebp. - Can I capture the whole page as one image?
- Yes:
image.fullPagecaptures the whole length of the page, andimage.clipSelectorone element. - Is the same URL rendered again each time?
- Yes. A URL render is never answered from an earlier result, because the page may have changed; every request loads it and counts.
- How long is the file kept?
- For the retention of your plan, one day on Free, 30 days on Starter and 365 days on Pro, or shorter with
expires_in. Download links are signed; fetch the file and store it yourself when you need it for longer.