A PDF generation API is two different products sold under one phrase, and picking the wrong kind wastes a week. One renders a PDF from markup or a template you design. The other converts a file you already have into PDF. They share almost no implementation and almost no pricing model.
Work out which you need before comparing anything.
Key takeaways
- Decide first whether you are designing a document or changing a file’s format.
- Renderers charge per document; converters charge against storage and delivery.
- Missing fonts substitute silently, so check the output rather than trusting it.
- Call
raise_for_status(), because an error body written to disk looks like a PDF. - Rendering belongs in a worker, not inside a web request.
The question that decides it
Ask what exists at the moment the PDF is required.
If the content is data, you need a renderer. An invoice assembled from database rows, a certificate with a name filled in, a monthly statement. There is no document yet; you are designing one and filling it. This is a layout problem, and the tools are HTML-to-PDF engines and template services.
If the content is already a file, you need a converter. A user uploaded a .docx and you want it viewable in the browser. A photograph has to become a one-page PDF for an archive. A document has to be normalised to A4 before printing. Nothing is being designed; a format is being changed.
Teams that buy a renderer for a conversion job end up writing HTML wrappers around files they already had. Teams that buy a converter for a rendering job discover there is nothing to convert.
Renderers, when you are designing the page
The dominant approach is to build the page as HTML and render it with a headless browser, which means your existing skills and CSS apply.
Running it yourself with Playwright:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content(html)
page.pdf(path="invoice.pdf", format="A4", print_background=True)
browser.close()
print_background=True is required for background colours and images. Without it, every background declared in your CSS is dropped from the output.
The costs of self-hosting a renderer are the same ones that apply to any headless browser. The container is large, each render spawns a process, memory usage is spiky, and concurrency needs bounding or a burst of requests takes the machine down. It works well and it is not free to operate.
Hosted renderers remove that operational surface and charge per document. The comparison between them comes down to which CSS features survive, whether headers and footers with page numbers are supported, and how templates are managed.
Things worth checking before committing to any renderer:
| Capability | Why it matters |
|---|---|
| Page numbers in headers and footers | Needs engine support; CSS alone does not do it reliably |
| Web fonts | A missing font silently substitutes and ruins the layout |
| Page break control | break-inside: avoid on table rows is what stops rows splitting |
| Background printing | Off by default nearly everywhere |
| Cold start time | Decides whether generation can be synchronous |
Converters, when the file already exists
This is the other half, and it is where a document API rather than a rendering engine is the right tool. The formats that can be converted into each other are listed on the file format conversion page.
With Filestack, conversion is a task applied to a stored file, addressed by its handle:
import requests
handle = "rW4VW6h7RYCHBLPiK3gq"
response = requests.get(f"https://cdn.filestackcontent.com/output=format:pdf/{handle}")
response.raise_for_status()
with open("out.pdf", "wb") as out:
out.write(response.content)
That turns an image into a PDF. No API key goes in the URL, because the handle already identifies the application. raise_for_status() matters here as much as the conversion itself, because a bad handle or an unsupported source format comes back as a plain-text error body, not a PDF, and writing that straight to disk leaves you with a file named out.pdf that is not one.
For documents that are already PDFs, pdfconvert normalises them, which is what you want before printing or merging files from mixed sources:
import requests
handle = "jLoemkNMSiyxhKHxo4G0" # a stored PDF, not an image
url = f"https://cdn.filestackcontent.com/pdfconvert=pageformat:a4,pageorientation:portrait/{handle}"
response = requests.get(url)
response.raise_for_status()
with open("normalised.pdf", "wb") as out:
out.write(response.content)
It also takes a pages argument, so extracting pages three to five is a URL rather than a library.
And pdfinfo reports what you are dealing with before you spend anything on it:
import requests
handle = "jLoemkNMSiyxhKHxo4G0" # a stored PDF, not an image
response = requests.get(f"https://cdn.filestackcontent.com/pdfinfo/{handle}")
response.raise_for_status()
info = response.json()
print(info["pages"], info["encrypted"], info["hasform"])
raise_for_status() earns its place again here. The error body for a bad handle is plain text, not JSON, so calling .json() straight off the response raises a confusing parsing error instead of the clear request error underneath it.
This is conversion, not rendering. It will turn images and documents into PDFs and reshape PDFs you already have. It will not lay out an invoice from database rows, and if that is your requirement, a renderer is the tool.
One conversion to know about in advance is PDF to .docx. Requesting output=format:docx on a PDF handle returns HTTP 400 with Conversion not supported: pdf -> docx. That is a limitation of the conversion pipeline rather than a plan boundary, so no tier unlocks it, and a workflow that depends on editable Word output needs a different approach.
Comparing the two approaches
For the conversion side on its own, without the generation question attached, the guide to converting file types works through the task syntax.
| Renderer | Converter | |
|---|---|---|
| Input | HTML, template plus data | an existing file |
| Typical use | invoices, certificates, reports | uploads, archives, normalisation |
| Design control | total | none, the source decides |
| Self-hosted cost | headless browser per render | not applicable |
| Fails when | CSS or fonts differ from the browser | source format is unsupported |
| Latency | scales with page count and browser startup | often faster once a result is cached |
Many products need both, and they sit at different points. Rendering happens when your application produces a document. Conversion happens when a user gives you one.
Generating from a URL
A third case is capturing a page that already exists on the web rather than markup you hold. That is a screenshot task rather than a rendering one. Unlike the delivery URLs elsewhere in this piece, urlscreenshot takes the web address in the handle position, so the request needs your API key in the path:
import os
import requests
API_KEY = os.environ["FILESTACK_API_KEY"]
response = requests.get(
f"https://cdn.filestackcontent.com/{API_KEY}/urlscreenshot/https://example.com"
)
response.raise_for_status()
with open("page.png", "wb") as out:
out.write(response.content)
What comes back is a PNG of the page, which can then become a PDF through the conversion above. It suits archiving and receipts, and it is not a substitute for a renderer when you need selectable text and controlled pagination, because a picture of a page has neither.
Pricing models, and why they differ so much
The two kinds of product charge differently, and comparing headline prices between them is meaningless.
Renderers charge per document, because each one costs them a browser process running for a second or more, and volume tiers matter because the underlying cost is real compute. A product generating a statement per customer per month can forecast this precisely, which is why per-document pricing suits it.
Converters charge against storage and delivery, because a conversion is a transformation applied on the way out and the result caches. The second request for the same converted file costs almost nothing, which is a completely different economic shape. It suits unpredictable volume, since a file nobody views again costs nothing to have converted.
The practical consequence is that a workload of ten thousand documents generated once and read once is expensive on a renderer and cheap on a converter, while a workload of ten documents generated from live data cannot use a converter at all. Model your own numbers against the shape rather than the sticker price.
Watch for two things in any pricing page here. Whether a failed render counts against your quota, because a template bug can burn a month’s allowance in an afternoon. And whether page count multiplies the cost, since a fifty-page report priced per page is a different product from one priced per document.
Getting the layout right the first time
For the rendering path, most of the work is not the API call. Three habits save the most time.
Build the template as a normal web page first. Open it in a browser, use print preview, and only move to the API once it looks right on paper. Debugging layout through a generated PDF is far slower than debugging it in developer tools.
Use a print stylesheet rather than fighting your screen styles. @media print lets you hide navigation, drop shadows and interactive elements without conditionals in the template. Renderers honour it, and the result is a page that stays readable when someone prints it from the browser too.
Fix the page size in CSS, not just in the API call. Setting @page { size: A4; margin: 20mm } means the template carries its own intent, so a change to the API call cannot silently reflow everything.
@page { size: A4; margin: 20mm; }
@media print {
nav, .no-print { display: none; }
table { break-inside: auto; }
tr { break-inside: avoid; break-after: auto; }
thead { display: table-header-group; }
}
That last block is the table fix.
What usually goes wrong
The format-by-format specifics, including which source types survive a trip to PDF intact, are in the guide to converting Pages documents to PDF.
Fonts. A font available on your development machine and absent on the render server substitutes silently. Everything shifts, nothing errors. Embed fonts or reference them by URL, and check the output rather than assuming.
Page breaks in tables. Long tables split rows across pages by default, leaving a header on one page and its data on the next. break-inside: avoid on rows and display: table-header-group on the header fixes it in most engines.
Images that have not loaded. A renderer that captures before images finish produces blank boxes. Wait for network idle rather than a fixed timeout.
Synchronous generation under load. Rendering takes long enough that doing it inside a web request is a bottleneck. Generate in a worker and deliver the result, especially for anything multi-page.
Absolute versus relative URLs. A renderer given HTML without a base URL cannot resolve relative paths, so stylesheets and images vanish. Set the base or make every reference absolute.
Questions people ask
Can one API do both?
Some do. The test is what the main endpoint requires with no input file. A product that requires a source document is a converter, and a product that accepts only markup cannot help with an uploaded .docx. The required parameters answer it.
Is HTML to PDF accurate?
For layout you designed, yes, provided fonts are embedded and you tested in the same engine. For arbitrary pages from the web, less so, because sites are built for screens and often carry no print stylesheet at all.
How do I add page numbers?
Through the renderer’s header and footer template, not CSS. Browser print engines do not expose page counters to page content reliably, which is why hosted renderers carry a separate parameter for it. If page numbers matter, check that parameter exists before choosing.
What about very large documents?
Anything past a few hundred pages is worth generating in sections and merging, because a single render holding the whole document in memory is where headless browsers fall over. Merging is a cheap operation compared with rendering.
Should the PDF be generated on demand or stored?
Store it if the content is fixed once created, like an invoice for a completed order. Generate on demand only when the content genuinely changes, and even then consider caching by a key derived from the data, since the same statement is usually requested more than once.
Deciding
If you are producing documents from your own data, take a renderer and budget time for fonts and page breaks, which are where the work actually is.
If you are handling files users give you, take a converter and let the format change happen close to storage rather than in your application.
Where the documents arriving need reading as well as converting, that is a third capability again, and recognition is what turns a scanned page into text. The document capture and data extraction page covers which plans include it.
That combination is common in practice. Documents arrive, get normalised to PDF, get read, and the extracted text feeds something else. Generation is one step among several rather than the product.
Joshua is a web developer with over 4 years of experience building responsive, high-performance websites and web applications. Currently working as an AI Automation Specialist, he combines modern web development with automation to create efficient, scalable digital solutions. He shares practical insights on WordPress, web development, and emerging technologies.
Read More →