Requirements
- Python 3.10 or later. This script uses only the standard library; no pip install is required.
- A Templatr account, an API key and available PDF quota.
- A terminal with curl to upload the template once. The environment commands below use a POSIX shell.
1. Prepare a reusable HTML template
Download this file as invoice-template.html. The template uses nested variables and an items loop. The loop replaces item with each object in the items array; data values are escaped as HTML. Keep styling inside the template: JavaScript is disabled during rendering.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Invoice {{invoice.number}}</title>
<style>
@page { size: A4; margin: 18mm; }
body { font: 11pt Arial, sans-serif; color: #172033; }
h1 { font-size: 26pt; margin-bottom: 8mm; }
table { width: 100%; border-collapse: collapse; margin: 8mm 0; }
th, td { padding: 3mm; border-bottom: 1px solid #dbe2ea; text-align: left; }
th:last-child, td:last-child { text-align: right; }
thead { display: table-header-group; }
tr, .total { break-inside: avoid; }
.total { text-align: right; font-weight: bold; }
</style>
</head>
<body>
<h1>Invoice {{invoice.number}}</h1>
<p>From: {{seller.name}}</p>
<p>Bill to: {{customer.name}}</p>
<p>Date: {{invoice.date}}</p>
<table>
<thead><tr><th>Description</th><th>Quantity</th><th>Amount</th></tr></thead>
<tbody>{{ items loop }}<tr><td>{{item.description}}</td><td>{{item.quantity}}</td><td>{{item.amount}}</td></tr>{{ end loop }}</tbody>
</table>
<p class="total">Total: {{invoice.total}}</p>
<p>Sample document with fictional data. No payment is due.</p>
</body>
</html>
2. Prepare the JSON values
Download invoice-data.json in the same directory. The JSON object is the request body itself, without an extra data wrapper. Format dates, currency and totals in your application. Templatr inserts display values; it does not calculate a total from the rows.
{
"seller": { "name": "Acme Studio" },
"customer": { "name": "Northstar Co." },
"invoice": { "number": "DEMO-0042", "date": "2026-09-23", "total": "EUR 299.00" },
"items": [
{ "description": "Template design", "quantity": 1, "amount": "EUR 199.00" },
{ "description": "Integration review", "quantity": 2, "amount": "EUR 100.00" }
]
}
3. Upload once and save the template ID
Create an API key in Settings, then set TEMPLATR_API_KEY in your server environment. Run this command from the directory containing the template. The JSON response contains template_id. Set TEMPLATR_TEMPLATE_ID to that value and keep it for later generations; you do not need to upload the same HTML for each document.
export TEMPLATR_API_KEY='YOUR_API_KEY'
curl --fail-with-body "https://api.templatr.app/upload" \
-H "Authorization: Bearer $TEMPLATR_API_KEY" \
-F "[email protected]" \
-F "name=Example invoice"
export TEMPLATR_TEMPLATE_ID='TEMPLATE_ID_FROM_RESPONSE'4. Generate and save the PDF
Save the script below beside invoice-data.json. It sends the JSON to the binary PDF endpoint, checks the HTTP status and content type, and writes invoice.pdf only when the response begins with a PDF header. Keep the API key in server-side environment variables, never in a frontend bundle or public repository.
urllib sends UTF-8 JSON and places Authorization in a header. HTTP errors stop the script before the file is written.
import json
import os
import sys
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
api_key = os.environ.get("TEMPLATR_API_KEY")
template_id = os.environ.get("TEMPLATR_TEMPLATE_ID")
if not api_key or not template_id:
raise SystemExit("Set TEMPLATR_API_KEY and TEMPLATR_TEMPLATE_ID first.")
input_path = Path(sys.argv[1] if len(sys.argv) > 1 else "invoice-data.json")
data = json.loads(input_path.read_text(encoding="utf-8"))
base_url = os.environ.get("TEMPLATR_API_BASE_URL", "https://api.templatr.app")
request = Request(
f"{base_url}/pdf/{quote(template_id, safe='')}?format=pdf",
data=json.dumps(data).encode("utf-8"),
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=60) as response:
if response.headers.get_content_type() != "application/pdf":
raise SystemExit("Expected an application/pdf response.")
pdf = response.read()
except HTTPError as error:
raise SystemExit(f"PDF request failed (HTTP {error.code}). Check the API error guide.") from None
if not pdf.startswith(b"%PDF-"):
raise SystemExit("Response does not contain a PDF.")
output_path = Path(sys.argv[2] if len(sys.argv) > 2 else "invoice.pdf")
output_path.write_bytes(pdf)
print("PDF saved.")
5. Run the script
The first argument is the JSON input and the second is the PDF output. Set the environment variables in the same terminal.
python3 generate_pdf.py invoice-data.json invoice.pdfChecks and troubleshooting
- Open invoice.pdf and check DEMO-0042, Northstar Co., both item rows and the EUR 299.00 total.
- Change the customer name and one description in invoice-data.json, run the script again, and check that the new values appear.
- Try long names and descriptions before using your production data. Missing variables render as empty strings.
- For HTTP 401, verify the API key. For 404, verify that the template belongs to that key’s account. For 429, check the account quota and Retry-After header.
- A timeout does not prove that the server failed to generate a document. Check document history before retrying; repeated successful requests can consume additional quota.