A URL-to-PDF job is a public-web fetch. POST /v1/pdf with url tells RelayPDF’s Chromium to open that address from the worker network. Private, loopback, and metadata hosts are rejected (error code url_not_allowed). A login wall on a public host is still a public URL; the worker is not your user. If the HTML you need is already in your backend, send html instead. That is the default for private documents. Product pages: https://relaypdf.com/url-to-pdf and https://relaypdf.com/html-to-pdf.
What URL fetch will and will not do
POST /v1/pdf accepts exactly one source: html, url, markdown, or templateId. url must be a public http(s) address. The worker cannot reach RFC1918 space, localhost, or cloud metadata endpoints. callbackUrl, when you use async jobs, must be https. Those rules are on the docs hub and the PDF endpoint page.
Chromium then prints whatever that public navigation returns. If the site answers with a sign-in form, the PDF is the sign-in form. If it answers with a 403 page, that is the PDF. The API key authenticates you to RelayPDF. It does not authenticate Chromium to the target site.
Rendering options that apply to a URL job are the same object as HTML: format (default letter), landscape, printBackground (default true), preferCSSPageSize, scale, margin, headerTemplate / footerTemplate, pageRanges, width / height, waitUntil (default networkidle0), timeout (milliseconds, max 60000), waitForSelector, waitForTimeout, plus two navigation fields: extraHTTPHeaders and cookies. Field names are identical in JSON, Node, and Python. List: https://relaypdf.com/docs/options. Endpoint: https://relaypdf.com/docs/pdf.
Three ways to get a private document into a PDF
You have three documented shapes. They are not interchangeable.
| Approach | When it fits | What you send |
|---|---|---|
| html on POST /v1/pdf | Preferred for anything private | UTF-8 HTML your backend already rendered |
| options.cookies on a url job | Public host; you already hold a cookie your own app issued for this job | url plus cookies[] with name and value |
| options.extraHTTPHeaders on a url job | Public host; the document endpoint accepts a header your service is allowed to send | url plus extraHTTPHeaders object |
Prefer html. Your process already has the data. You do not send session material to a third-party browser. Relative assets still have to be public URLs or inlined; that is an HTML problem, not a login problem. Use the HTML product path when the document is an invoice, a tenant report, or any page your server can assemble: https://relaypdf.com/html-to-pdf.
options.cookies
On a url job, options.cookies is an array. Each item requires name and value. Optional fields in the public contract are domain, path, and url. Chromium attaches those cookies to matching requests while it fetches the page. extraHTTPHeaders is a string-to-string object sent by Chromium on url fetches. Both fields also exist on POST /v1/images with the same names.
Use cookies only for a page you operate, on a public hostname, when you already have a cookie your application issued for this export. Do not collect cookies from end-user browsers. Do not replay someone else’s session. Do not treat this as a way to reach an internal admin host; those hosts fail url_not_allowed before Chromium runs.
curl -X POST https://api.relaypdf.com/v1/pdf
-H "Authorization: Bearer pdf_live_…"
-H "Content-Type: application/json"
-d '{
"url": "https://reports.example.com/weekly",
"filename": "weekly.pdf",
"options": {
"format": "A4",
"printBackground": true,
"cookies": [
{
"name": "export_token",
"value": "job-scoped-value",
"domain": "reports.example.com",
"path": "/"
}
]
}
}'
--output weekly.pdf
The cookie name and value above are placeholders for a token your own service created for this render. Scope it to the export, expire it quickly, and bind it to the document you intend to print. That is application design on your side. RelayPDF only forwards the cookie Chromium is told to send.
options.extraHTTPHeaders
extraHTTPHeaders is the other documented navigation hook. Values are strings. Typical keys on a service you own are Accept-Language or a custom header your origin already documents for machine access. The header is sent on Chromium’s url fetches for that job. It is not a substitute for html when the page is private.
import os
from relaypdf import RelayPDF, RelayPDFError
client = RelayPDF(api_key=os.environ["RELAYPDF_API_KEY"])
try:
pdf = client.pdf.from_url(
"https://reports.example.com/weekly",
filename="weekly.pdf",
options={
"format": "A4",
"printBackground": True,
"extraHTTPHeaders": {
"X-Export-Job": "weekly-2026-08-23",
},
},
)
pdf.save("weekly.pdf")
except RelayPDFError as err:
print(err.status, err.code, err.message)
JSON bodies keep REST camelCase (extraHTTPHeaders, printBackground). Python methods are snake_case (from_url). Errors are { error: { code, message } }. Failed jobs are not billed. url_not_allowed is the code when the host is private, loopback, or metadata. render_failed is the code when Chromium does not finish inside options.timeout (max 60s). That timeout is the render budget, not the SDK HTTP timeout.
Why html is the private-page path
A login-gated dashboard is usually the wrong URL to hand a hosted browser. Even if the hostname is public, the interesting bytes are produced after your app authorizes a user. Your backend already did that work when it built the page. Post the HTML.
Sending html also avoids a second class of failures: subresources. A URL job that clears the HTML document can still print a broken layout if CSS, fonts, or XHR endpoints stay gated. extraHTTPHeaders and cookies apply to Chromium fetches for that job, but you still depend on every asset URL being reachable from the worker and allowed by the same public-host rule. Inline what you can. Point the rest at public CDNs or signed public URLs you already issue.
Handlebars is the third documented source if the document is a published template plus JSON. That is templateId + templateData, not a crawl of an authenticated app. Office files are POST /v1/convert, not this endpoint.
curl -X POST https://api.relaypdf.com/v1/pdf
-H "Authorization: Bearer pdf_live_…"
-H "Content-Type: application/json"
-d '{
"html": "<!doctype html><html><body><h1>Weekly report</h1><p>Rendered in-app.</p></body></html>",
"filename": "weekly.pdf",
"options": { "format": "A4", "printBackground": true }
}'
--output weekly.pdf
Wait options that are not login
People sometimes treat waitUntil as a way to “get past” a gate. It is not. waitUntil is a Chromium lifecycle: load, domcontentloaded, networkidle0 (default), networkidle2. waitForSelector takes a CSS selector or { selector, timeout, visible }. waitForTimeout is an extra wait in milliseconds, max 30000. Those fields wait for the public page to settle. They do not submit a password form and they are not documented as an auth API.
Response modes
Default response is binary: 200 application/pdf with x-relaypdf-id, x-relaypdf-size, and content-disposition. response: url returns JSON with a public download that expires in 24 hours (GET /v1/files/:id, no key). response: async returns 202 with a poll URL; poll GET /v1/jobs/:id with the same API key, or set callbackUrl. Keys are Authorization: Bearer pdf_live_… from the dashboard. Query-string keys are not accepted. CLI credentials live in ~/.config/relaypdf/credentials.json with mode 0600 and are never printed.
What this post will not cover
This is not a guide to taking cookies from browsers, bypassing access control, or reaching hosts the worker blocks. If you do not already have a lawful, application-issued token for a public page you operate, use html. If the host is not public, use html. Product entry points remain https://relaypdf.com/url-to-pdf for public pages and https://relaypdf.com/html-to-pdf for documents you already hold.