Disclosure: RenderScreenshot is our product and renders with Chrome. DocRaptor is not ours. The engine tests below ran on 2026-10-18 on our machine; DocRaptor's facts and prices come from its API reference and plans page, read the same day.
DocRaptor is a hosted front end for Prince, a typesetting engine built for print. That is its strength and the reason people look for an alternative: Prince is excellent at books, footnotes and page references, but your web app is built for Chrome, and every CSS feature you use has to be checked against a second engine. A Chrome-based API prints what your users already see.
The honest question is which print features you would lose. We wrote one test file that uses ten paged-media features, printed it with Prince 17 and with Chrome 148, and checked every page. Here is what we found, followed by a parameter map, prices at three volumes and migration code. For the wider market, see our HTML to PDF API roundup.
The short answer
| Choose DocRaptor (Prince) if | Choose a Chrome-based API if |
|---|---|
| Your PDFs need footnotes, running chapter titles in the header, or "see page 12" cross-references | Your PDFs are invoices, receipts, statements, dashboards or exported app pages |
You need PDF/A or PDF/UA output on request (prince_options.profile) |
Your templates use the CSS and JavaScript your web app uses |
| You also need XLS or XLSX files from the same API | You also need PNG or WebP screenshots from the same key |
| Each document may take minutes to lay out (async jobs up to 600 seconds) | Documents render within one minute |
| You price per document at any size and accept a higher entry price | You want the lower price per document at 1,000 and 10,000 a month |
The paged-media test
Method. One HTML file (paged.html, five pages) with ten paged-media features. Each feature prints a marker string, so pdftotext and pdfinfo can show whether it worked. Prince 17 ran from the prince-pdf Python package (the free version, which adds a small logo to page 1). Chrome 148.0.7778.97 ran through Puppeteer 24.43.1 with page.pdf({ printBackground: true, preferCSSPageSize: true }). Both on macOS, Apple M1 Pro, 2026-10-18.
| Paged-media feature | CSS used | Prince 17 | Chrome 148 |
|---|---|---|---|
| Page counter in a margin box | @bottom-center { content: counter(page) " of " counter(pages) } |
Yes | Yes |
| Different first page | @page :first { ... } |
Yes | Yes |
| Static text in another margin box | @bottom-left { content: "..." } |
Yes | Yes |
| Named page with its own size | page: wide + @page wide { size: A4 landscape } |
Yes, page 4 landscape | Yes, page 4 landscape |
| Page break before each chapter | break-before: page |
Yes | Yes |
| Running header with the chapter title | string-set: chapter content() + string(chapter) |
Yes | No, header empty |
| Table of contents with page numbers | target-counter(attr(href), page) |
Yes, "2", "3", "4" | No, numbers missing |
| Dot leaders in the contents | leader('.') |
Yes | No |
| Cross-reference "(page 4)" | target-counter() in ::after |
Yes | No, the whole ::after text was dropped |
| Footnote at the foot of the page | float: footnote |
Yes, moved to the page bottom | No, stayed inline in the paragraph |
Two more differences showed up in the file metadata:
- Bookmarks. Prince wrote a PDF outline (bookmarks from the headings) by default. Chrome wrote none.
- Tagging. Chrome's PDF was tagged (
pdfinforeportsTagged: yes). Prince's was not, by default; Prince tags when you ask for it, for example with the PDF/UA-1 profile.
The result is a clear line. Chrome now handles the page furniture most business documents need: page X of Y, a different cover page, landscape pages inside a portrait document. Prince still owns the book features: running headers that follow the content, generated page references and footnotes.
A caution about versions
We tested Prince 17 because it is the current release. DocRaptor's API reference lists pipeline 10.1, which is Prince 15.1, as its default. Versions matter: in our DocRaptor vs PDFShift test three days earlier, DocRaptor's default pipeline stacked a three-column CSS grid into one column. Prince 17 on our machine laid the same kind of grid out correctly, along with flexbox gap, :has() and CSS nesting. It still ignored aspect-ratio, object-fit and color-mix(). Check which pipeline your account uses before you blame the CSS.
If you move to Chrome: replacing the missing features
| Prince feature | Chrome replacement |
|---|---|
Running chapter header (string-set) |
A fixed pdf.header template with the document title class, or render each chapter as its own request and merge |
Contents page numbers (target-counter) |
Drop the numbers and rely on the PDF being read on screen, or render twice: print once, find each heading's page with pdftotext -f N -l N, write the numbers into the HTML, print again |
Footnotes (float: footnote) |
Endnotes at the end of each section, linked with anchors |
| Bookmarks | None in the API; add them afterwards with a PDF library if you need them |
| PDF/A, PDF/UA profiles | None; stay on a print engine if a regulator asks for these |
Parameter map
DocRaptor names are from its API reference; ours are from the PDF parameters and content input docs.
| Task | DocRaptor | RenderScreenshot |
|---|---|---|
| Endpoint | POST https://api.docraptor.com/docs |
POST https://api.renderscreenshot.com/v1/screenshot |
| Auth | API key as the HTTP Basic user name | Authorization: Bearer rs_live_... |
| HTML input | document_content |
html |
| URL input | document_url |
url |
| Base URL for relative assets | prince_options.baseurl |
html_base_url |
| Output type | type: "pdf" (also xls, xlsx) |
output.format: "pdf" (also png, jpeg, webp) |
| Print or screen CSS | prince_options.media, default print |
browser.media, default screen |
| Paper size | CSS @page { size } |
pdf.paper, or your @page size with pdf.prefer_css_page_size: true |
| Page numbers | CSS margin boxes | CSS margin boxes, or a pdf.footer template |
| JavaScript | Off unless javascript: true |
Always on |
| Wait for async content | Page calls docraptorJavaScriptFinished() |
wait.until: "networkidle" or wait.for_selector |
| Free test output | test: true, watermarked |
Free plan credits each month |
| Long jobs | async: true + callback_url, up to 600 s |
webhook.url, returns 202 with a job ID |
| Hosted file link | hosted: true (paid add-on) |
X-Cache-URL header on every render |
| Passwords and permissions | prince_options.user_password, disallow_print, ... |
None |
| Synchronous time limit | 60 seconds | wait.timeout up to 60 seconds |
Two rows cause most migration bugs. Media: DocRaptor applies @media print rules by default, we apply screen rules, so send browser.media: "print". Paper: our default is Letter. If your templates set @page { size: A4 }, add prefer_css_page_size: true or set pdf.paper yourself. In our Chrome test, the @page margins in the template were applied either way.
Prices at three volumes
DocRaptor's monthly plans as listed on its plans page on 2026-10-18; ours from our pricing page the same day. Each cell is the cheapest plan that covers the volume.
| PDFs per month | DocRaptor | RenderScreenshot |
|---|---|---|
| 1,000 | $75 (Premium, 1,250 documents) | $10 (Starter) |
| 10,000 | $399 (Bronze, 15,000 documents) | $40 (Growth) |
| 100,000 | Enterprise, contact sales (Silver is $1000 for 40,000) | Above our largest plan (60,000 credits); not self-serve today |
DocRaptor charges per document at any size and lets paid plans go over their limit at a per-document rate. We charge one credit per successful render at any size, and stop at the limit instead of billing overage. At 100,000 documents a month neither of us has a list price; talk to DocRaptor, or run Chrome yourself.
Migration code
The pattern is the same in every language: one render_pdf(html) function, a provider switch in an environment variable, and nothing else in your app knows which service printed the file. The Node version can also render with the second provider in the background and log both page counts, which is the cheapest way to find templates that break.
We checked the syntax of all three scripts and the page-count helper (against Chrome and Prince PDFs from our test) on 2026-10-18. They were not run against either live API for this page.
Node.js (22+)
// One renderPdf() for both providers. PDF_PROVIDER picks the primary; PDF_SHADOW renders with the other // in the background and logs the page counts, so you can compare before you switch. import { writeFile } from 'node:fs/promises'; const providers = { async docraptor(html) { const auth = Buffer.from(`${process.env.DOCRAPTOR_API_KEY}:`).toString('base64'); const res = await fetch('https://api.docraptor.com/docs', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Basic ${auth}` }, body: JSON.stringify({ type: 'pdf', document_content: html, test: process.env.NODE_ENV !== 'production' }), signal: AbortSignal.timeout(65_000), }); if (!res.ok) throw new Error(`DocRaptor ${res.status}: ${await res.text()}`); return Buffer.from(await res.arrayBuffer()); }, async renderscreenshot(html) { const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.RS_API_KEY}` }, body: JSON.stringify({ html, output: { format: 'pdf' }, browser: { media: 'print' }, // DocRaptor applies print CSS by default pdf: { prefer_css_page_size: true }, // keep the @page size your templates already set wait: { until: 'networkidle' }, }), signal: AbortSignal.timeout(65_000), }); if (!res.ok) throw new Error(`RenderScreenshot ${res.status}: ${await res.text()}`); return Buffer.from(await res.arrayBuffer()); }, }; const pageCount = (pdf) => (pdf.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length; export async function renderPdf(html) { const primary = process.env.PDF_PROVIDER ?? 'docraptor'; const pdf = await providers[primary](html); const shadow = process.env.PDF_SHADOW; if (shadow && shadow !== primary) { providers[shadow](html) .then((other) => console.log(JSON.stringify({ shadow, pages: [pageCount(pdf), pageCount(other)], bytes: [pdf.length, other.length] }))) .catch((err) => console.error(`shadow ${shadow} failed:`, err.message)); } return pdf; } if (import.meta.url === `file://${process.argv[1]}`) { const html = '<!doctype html><style>@page { size: A4; margin: 18mm }</style><h1>Statement</h1><p>Balance due: $120.00</p>'; await writeFile('statement.pdf', await renderPdf(html)); console.log('saved statement.pdf'); }
Run PDF_PROVIDER=docraptor PDF_SHADOW=renderscreenshot node render-pdf.mjs for a week. Every line where the two page counts differ points at a template to inspect. While DocRaptor stays in test mode outside production, its shadow renders are free and watermarked.
Python
"""render_pdf(html) for DocRaptor or RenderScreenshot, picked by PDF_PROVIDER.""" import os import requests def _docraptor(html): resp = requests.post( 'https://api.docraptor.com/docs', auth=(os.environ['DOCRAPTOR_API_KEY'], ''), json={'type': 'pdf', 'document_content': html, 'test': os.environ.get('ENV') != 'production'}, timeout=65, ) resp.raise_for_status() return resp.content def _renderscreenshot(html): resp = requests.post( 'https://api.renderscreenshot.com/v1/screenshot', headers={'Authorization': f"Bearer {os.environ['RS_API_KEY']}"}, json={ 'html': html, 'output': {'format': 'pdf'}, 'browser': {'media': 'print'}, 'pdf': {'prefer_css_page_size': True}, 'wait': {'until': 'networkidle'}, }, timeout=65, ) resp.raise_for_status() return resp.content def render_pdf(html): provider = os.environ.get('PDF_PROVIDER', 'docraptor') return {'docraptor': _docraptor, 'renderscreenshot': _renderscreenshot}[provider](html)
Ruby
# render_pdf(html) for DocRaptor or RenderScreenshot, picked by PDF_PROVIDER. require 'json' require 'net/http' module PdfRenderer module_function def render_pdf(html) case ENV.fetch('PDF_PROVIDER', 'docraptor') when 'docraptor' then docraptor(html) when 'renderscreenshot' then renderscreenshot(html) else raise ArgumentError, "unknown PDF_PROVIDER #{ENV['PDF_PROVIDER']}" end end def docraptor(html) req = Net::HTTP::Post.new(URI('https://api.docraptor.com/docs'), 'Content-Type' => 'application/json') req.basic_auth(ENV.fetch('DOCRAPTOR_API_KEY'), '') req.body = { type: 'pdf', document_content: html, test: ENV['RAILS_ENV'] != 'production' }.to_json send_request(req) end def renderscreenshot(html) req = Net::HTTP::Post.new(URI('https://api.renderscreenshot.com/v1/screenshot'), 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}") req.body = { html: html, output: { format: 'pdf' }, browser: { media: 'print' }, pdf: { prefer_css_page_size: true }, wait: { until: 'networkidle' } }.to_json send_request(req) end def send_request(req) res = Net::HTTP.start(req.uri.host, req.uri.port, use_ssl: true, read_timeout: 65) { |http| http.request(req) } raise "#{req.uri.host} #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) res.body end end
Templates that use prince-* properties, string-set or float: footnote will render without those effects in Chrome, as the test table shows. Search your templates for those names first; they are the work in a migration.
When to stay with DocRaptor
- Your PDFs are books, manuals, contracts with numbered clauses and cross-references, or academic papers. Prince's generated content is the reason DocRaptor exists, and Chrome has no answer for footnotes or page references.
- Compliance asks for PDF/A or PDF/UA. DocRaptor exposes Prince's profiles on the request.
- You need Excel output or password-protected PDFs. Neither is in our API.
- Some documents take several minutes. DocRaptor's async mode allows 600 seconds per job; our wait timeout is 60 seconds.
If your PDFs are business documents that already look right in Chrome, get a free API key and run the shadow script above on a week of real templates. The PDF API overview covers header and footer templates and the print CSS checklist.
Last tested 2026-10-18: Prince 17 (prince-pdf 17.0.0.post2) and Chrome 148.0.7778.97 via Puppeteer 24.43.1, Node 22.18.0, macOS on an Apple M1 Pro. DocRaptor parameters and plans read from its site on the same date.