How to Use Html2Pdf.app with Python Requests
Send HTML or a public URL to Html2Pdf.app with Python requests, handle PDF bytes safely, configure rendering, and troubleshoot common errors.
Use Python’s requests library to POST JSON to https://api.html2pdf.app/v1/generate, authenticate with the X-API-Key header, check the HTTP status, and write the successful response body as binary PDF data. The required JSON field is html; it can contain raw HTML or a publicly reachable URL.
Requirements and setup
- Use Python 3.10 or newer.
- Install Requests:
python -m pip install requests - Create an Html2Pdf.app API key and supply it to your server process as
HTML2PDF_API_KEY.
Keep the key in trusted backend code, server-side scripts, or trusted jobs. Do not put it in browser JavaScript, public repositories, or client-side templates. An environment variable keeps it out of the source file.
Minimal runnable Python example
This example converts a public page and saves the PDF returned by the synchronous API call:
import os
from pathlib import Path
import requests
api_key = os.environ["HTML2PDF_API_KEY"]
response = requests.post(
"https://api.html2pdf.app/v1/generate",
json={"html": "https://www.example.com"},
headers={"X-API-Key": api_key},
timeout=60,
)
response.raise_for_status()
Path("document.pdf").write_bytes(response.content)
Set the environment variable before running the script. For example, on macOS or Linux:
export HTML2PDF_API_KEY="your_api_key"
python convert.py
The successful response is binary PDF content. Do not parse it as JSON or decode it as text. Check the status first, then write response.content as bytes.
Convert inline HTML and set page options
Pass rendering options alongside html in the JSON body. This example converts inline markup and sets page size, media mode, margins, and filename:
import os
from pathlib import Path
import requests
payload = {
"html": "<h1>Invoice</h1><p>Total: $240.00</p>",
"format": "A4",
"media": "print",
"marginTop": 40,
"marginRight": 32,
"marginBottom": 40,
"marginLeft": 32,
"filename": "invoice.pdf",
}
response = requests.post(
"https://api.html2pdf.app/v1/generate",
json=payload,
headers={"X-API-Key": os.environ["HTML2PDF_API_KEY"]},
timeout=60,
)
response.raise_for_status()
Path("invoice.pdf").write_bytes(response.content)
The API accepts either raw HTML markup or a URL in html. Prefer POST with JSON: it avoids query-string escaping and length problems, especially for raw HTML or long templates. GET is supported, but its query parameters must be URL-encoded and the documentation cautions against using GET for raw HTML or long values.
Rendering options
Options belong in the JSON request body. The documented controls include:
| Option | What it controls |
|---|---|
format |
Standard paper formats: Letter, Legal, Tabloid, Ledger, and A0 through A6. |
| Orientation | Portrait or landscape page orientation. |
| Custom dimensions | Custom page width and height when a standard format does not fit. |
| Margins | Top, right, bottom, and left margins in pixels. |
filename |
Suggested PDF filename. |
media |
CSS media mode, such as print or screen. |
| Scale | Rendering scale. |
| Header and footer | Templates for page headers and footers. |
| Password and permissions | PDF password and permission settings. |
waitFor |
A delay from 0 to 10 seconds to allow JavaScript or asynchronous resources to finish. |
Rendering depends on the selected CSS media mode, available fonts and other external resources, and JavaScript load timing. Use print when the page’s print styles define the intended document; use screen when its screen styles are the desired layout. Add a reasonable waitFor delay only when the page needs time to finish rendering dynamic content. Check the current provider documentation for exact parameter names and accepted values before relying on less common options.
cURL, Python, and Node.js request patterns
cURL
curl -X POST "https://api.html2pdf.app/v1/generate" \
-H "X-API-Key: $HTML2PDF_API_KEY" \
-H "Content-Type: application/json" \
--data '{"html":"https://www.example.com","format":"A4","media":"print"}' \
--output document.pdf
Python requests
response = requests.post(
"https://api.html2pdf.app/v1/generate",
json={"html": "https://www.example.com", "format": "A4"},
headers={"X-API-Key": os.environ["HTML2PDF_API_KEY"]},
timeout=60,
)
response.raise_for_status()
Path("document.pdf").write_bytes(response.content)
Node.js
const response = await fetch('https://api.html2pdf.app/v1/generate', {
method: 'POST',
headers: {
'X-API-Key': process.env.HTML2PDF_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html: 'https://www.example.com', format: 'A4' }),
signal: AbortSignal.timeout(60000),
});
if (!response.ok) {
throw new Error(`PDF conversion failed: HTTP ${response.status}`);
}
const pdf = Buffer.from(await response.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('document.pdf', pdf));
In each language, treat the synchronous success body as PDF bytes and keep the API key on the server. The Python and Node examples use a 60-second client timeout; adjust it to fit your application’s request and job limits.
Asynchronous conversion with a callback
For work that should outlive a single request, provide callBackUrl and optionally a state value to correlate the result with your job. The API returns 202 Accepted when it queues the conversion. That response is not the PDF.
import os
import requests
response = requests.post(
"https://api.html2pdf.app/v1/generate",
json={
"html": "https://www.example.com",
"callBackUrl": "https://your-domain.example/hooks/pdf-ready",
"state": "invoice-8472",
},
headers={"X-API-Key": os.environ["HTML2PDF_API_KEY"]},
timeout=30,
)
response.raise_for_status()
if response.status_code != 202:
raise RuntimeError(f"Expected queued response, got HTTP {response.status_code}")
When conversion finishes, the provider POSTs JSON to the callback endpoint. The document field contains base64-encoded PDF bytes, and the submitted state is returned unchanged. Decode the document before saving:
import base64
from pathlib import Path
# callback_data is the JSON object parsed from the callback request body
pdf_bytes = base64.b64decode(callback_data["document"], validate=True)
Path("document.pdf").write_bytes(pdf_bytes)
Use a publicly reachable HTTPS callback URL and make callback processing idempotent: delivery may be attempted more than once, and failed callback deliveries are retried up to three times according to the documentation. Store the job state so duplicate delivery does not create duplicate downstream work.
Reliability, performance, and cost considerations
- Timeouts: A synchronous call stays open while conversion runs. Set an explicit client timeout appropriate for your own request budget. For longer work, callback mode separates queuing from completion.
- Retries: The documented errors include HTTP 500 for an unhandled server error. Retry that case after a short delay with increasing delays between attempts. Avoid retrying immediately in a tight loop.
- Rendering time: JavaScript execution and remote CSS, fonts, and images affect when a document is ready. Use
waitForwithin its documented 0–10 second range when needed, and keep source resources reachable by the renderer. - Cost and limits: Plan limits can result in HTTP 403. Check your account’s plan and notification before retrying; the research materials do not provide current conversion prices or per-plan quantities, so consult the provider for those details.
- Data handling: The provider documentation says generated PDFs are processed temporarily and not permanently stored on its servers, and that raw HTML or text submitted in
htmlis not stored in conversion logs. It also says selected request metadata and a source URL provided inhtmlmay be retained. Consult its privacy policy and data processing agreement for details; this summary reflects the provider’s statement, not an independent audit.
Troubleshooting
| Symptom or status | Likely cause | Action |
|---|---|---|
| 400 Bad Request | The source URL is inaccessible or a parameter is invalid. | Confirm the URL is public and reachable, then check option names and values. |
| 401 Unauthorized | The API key is missing or invalid. | Check that HTML2PDF_API_KEY is set and that the request sends it in X-API-Key. |
| 403 Forbidden | The account reached a plan limit. | Review the account limit and notification. Resolve the limit before submitting again. |
| 500 Internal Server Error | An unhandled server error occurred. | Retry after a short delay with increasing delays between attempts. Contact support if it persists. |
| Saved file is not a valid PDF | An error response may have been written as though it were the successful PDF body. | Call raise_for_status() before writing bytes; handle the error response separately. |
| Blank page or missing styling | The URL may not be public to the renderer, or CSS, fonts, or images may be unreachable. Media mode or JavaScript timing may also differ from expectations. | Check resource reachability, select the intended media mode, and allow dynamic content time with waitFor when appropriate. |
| GET request fails with raw HTML | HTML was not URL-encoded or the query became too long. | Use POST with a JSON body for raw markup or long template content. |
| Callback handler saves corrupt output | The callback’s document value is base64 text, not PDF bytes. |
Base64-decode it, validate the input, then persist the resulting bytes. |
| Duplicate callback effects | A callback was delivered again after a failed delivery or acknowledgment. | Use the returned state or job identifier for idempotency and acknowledge only after safely recording the result. |
Do not retry 400, 401, or 403 responses until the invalid input, credentials, or account limit is corrected.
Or skip the browser setup
If your task is to capture a web page as an image, ScreenshotNeo provides a one-request screenshot API. See the ScreenshotNeo API documentation for the available parameters.
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo; this call returns a screenshot image, while Html2Pdf.app above generates a PDF.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Can html be a URL or must it be markup?
It can be either raw HTML markup or a publicly reachable URL. Use POST JSON for both, especially for raw markup.
Why does the API return 202 instead of a PDF?
When you provide callBackUrl, the request queues asynchronous work. The PDF arrives later in the callback payload as base64 data.
Should I save response.text or response.json()?
For a successful synchronous conversion, save response.content as binary data. The response body is the PDF itself.
Can I call the API directly from a webpage?
Keep the API key on a trusted server. A browser call would expose the key to visitors.
Sources and current options
See the Html2Pdf.app API documentation and its official Python integration guide for the current endpoint, options, and account details. Provider behavior and supported parameters can change; verify them there when implementing.


