Guide
Python PDF generation with a REST API
Generate PDFs in Python with requests: a small helper with error handling, and a Flask route that returns a PDF download. No system packages to install.
Updated
The approach
Python PDF libraries either draw pages by coordinates, which makes tables and page breaks your problem, or convert HTML, which needs system packages or a headless browser on every server. With a JSON to PDF API, Python builds a dictionary and sends it. Pagination, repeated table headers and page numbers are handled by the layout engine.
The examples use requests (pip install requests) and run on Python 3.9 or later.
Generate a PDF with requests
generate_pdf(document) returns the PDF as bytes and raises with the problem code when the API rejects the document. Set TRYDOCIFY_API_KEY first.
import os
import requests
API_URL = "https://api.trydocify.com/api/v1/generate"
def generate_pdf(document: dict) -> bytes:
response = requests.post(
API_URL,
json=document,
headers={"X-API-Key": os.environ["TRYDOCIFY_API_KEY"]},
timeout=30,
)
if not response.ok:
# RFC 9457 problem document with a stable "code"
problem = response.json()
raise RuntimeError(f"{response.status_code} {problem['code']}: {problem.get('detail')}")
return response.content
pdf = generate_pdf({
"test": True, # free, watermarked render while you build; remove for production
"title": "Monthly usage report",
"blocks": [
{"type": "paragraph", "text": "Usage for October 2026."},
{
"type": "table",
"columns": [{"header": "Service"}, {"header": "Requests", "align": "right"}],
"rows": [["Search", 18240], ["Exports", 3120]],
"footer": [["Total", 21360]],
},
],
})
with open("report.pdf", "wb") as f:
f.write(pdf)
print(f"Saved report.pdf ({len(pdf)} bytes)")"test": True makes the render free and watermarked while you work on the layout (200 test renders a day per key). Remove it in production. Validation errors are never charged, and their errors field names the exact JSON path to fix, such as blocks[1].rows[2].
Return a PDF from Flask
To serve a receipt or report on demand, build the document from your data, call the API and stream the bytes back with send_file:
import io
import os
import requests
from flask import Flask, abort, send_file
app = Flask(__name__)
API_URL = "https://api.trydocify.com/api/v1/generate"
@app.get("/orders/<order_id>/receipt.pdf")
def receipt(order_id: str):
document = {
"test": app.debug, # watermarked and free while developing
"title": f"Receipt for order {order_id}",
"blocks": [
{"type": "keyValue", "items": [{"key": "Order", "value": order_id}]},
{
"type": "table",
"columns": [{"header": "Item"}, {"header": "Amount", "align": "right"}],
"rows": [["Annual license", "$490.00"]],
"footer": [["Total", "$490.00"]],
},
],
}
response = requests.post(
API_URL,
json=document,
headers={"X-API-Key": os.environ["TRYDOCIFY_API_KEY"]},
timeout=30,
)
if not response.ok:
app.logger.error("PDF generation failed: %s", response.text)
abort(502)
return send_file(
io.BytesIO(response.content),
mimetype="application/pdf",
download_name=f"receipt-{order_id}.pdf",
)app.debugswitches test mode on while you develop, so local renders are free.download_namesets the file name. Addas_attachment=Trueto force a download instead of opening the PDF in the browser.- In Django, return
HttpResponse(response.content, content_type="application/pdf"). In FastAPI, useResponse(content=..., media_type="application/pdf").
Tips for production
- Always pass a
timeouttorequests.post; typical documents render in milliseconds. - Retry
429and503after the number of seconds inRetry-After. Other errors won't succeed on retry. - Format numbers and dates in Python before sending them. Values are printed exactly as written, so
f"${amount:,.2f}"gives you full control. - For images, base64-encode the file:
"data:image/png;base64," + base64.b64encode(data).decode().
The document format is the same in every language. The JSON to PDF tutorial walks through it block by block.
Try it with your own data
Every plan includes 200 free test renders a day. Add "test": true while you build, and remove it when you go live.