Python
The Transactional client API is a single POST. The standard library handles it without a dependency. httpx is the modern pick if you want async + retries.
Standard library only (urllib)
import json
import os
import urllib.request
import urllib.error
def generate_pdf(document_id: str, variables: dict) -> dict:
payload = json.dumps({
"documentId": document_id,
"variables": variables,
}).encode()
req = urllib.request.Request(
"https://api.transactional.dev/v1/generate",
method="POST",
headers={
"x-api-token": os.environ["TRANSACTIONAL_API_TOKEN"],
"Content-Type": "application/json",
},
data=payload,
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body = json.loads(e.read() or b"{}")
raise RuntimeError(
f"Transactional {e.code}: {body.get('error', 'unknown')} — {body.get('message', '')}"
) from e
result = generate_pdf(
"1d8e5d56-1a4f-4b62-8c33-2d34a64b2f00",
{
"customer": {"name": "Acme Corp"},
"invoice": {"number": "INV-2026-0142", "total": 1280.50},
},
)
print(result["url"])
httpx with retries (sync + async)
import os
import httpx
TRANSACTIONAL_URL = "https://api.transactional.dev/v1/generate"
transport = httpx.HTTPTransport(retries=2)
def generate_pdf(document_id: str, variables: dict) -> dict:
with httpx.Client(transport=transport, timeout=30) as client:
r = client.post(
TRANSACTIONAL_URL,
json={"documentId": document_id, "variables": variables},
headers={"x-api-token": os.environ["TRANSACTIONAL_API_TOKEN"]},
)
if r.is_success:
return r.json()
body = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
raise TransactionalError(r.status_code, body.get("error", "unknown"), body.get("message", ""))
class TransactionalError(Exception):
def __init__(self, status: int, code: str, message: str) -> None:
super().__init__(f"Transactional {status}: {code} — {message}")
self.status = status
self.code = code
Async version (same shape):
async def generate_pdf_async(document_id: str, variables: dict) -> dict:
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(
TRANSACTIONAL_URL,
json={"documentId": document_id, "variables": variables},
headers={"x-api-token": os.environ["TRANSACTIONAL_API_TOKEN"]},
)
r.raise_for_status()
return r.json()
Branching on error codes
try:
result = generate_pdf(uuid, variables)
except TransactionalError as e:
if e.code == "quota_exceeded":
raise OutOfCredits()
if e.code in ("NOT_FOUND", "invalid_document_id"):
raise BadTemplate(uuid)
if e.code == "UNAUTHORIZED":
raise ApiTokenInvalid()
raise
Django integration
# settings.py
TRANSACTIONAL_API_TOKEN = os.environ["TRANSACTIONAL_API_TOKEN"]
TRANSACTIONAL_INVOICE_TEMPLATE = os.environ["TRANSACTIONAL_INVOICE_TEMPLATE_UUID"]
# A view that streams the PDF inline as the response
from django.http import HttpResponse
from django.conf import settings
import httpx
def download_invoice(request, invoice_id):
invoice = get_object_or_404(Invoice, id=invoice_id, user=request.user)
res = httpx.post(
"https://api.transactional.dev/v1/generate",
json={
"documentId": settings.TRANSACTIONAL_INVOICE_TEMPLATE,
"variables": invoice.template_variables(),
},
headers={"x-api-token": settings.TRANSACTIONAL_API_TOKEN},
timeout=30,
)
res.raise_for_status()
pdf = httpx.get(res.json()["url"], timeout=30).content
response = HttpResponse(pdf, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="invoice-{invoice.number}.pdf"'
return response
For background generation, push to Celery:
@shared_task(bind=True, autoretry_for=(httpx.HTTPError,), max_retries=3, retry_backoff=True)
def generate_invoice_pdf(self, invoice_id):
invoice = Invoice.objects.get(id=invoice_id)
# ... same as above, then save to S3 / Django storages
FastAPI integration
from fastapi import FastAPI, HTTPException
import httpx
import os
app = FastAPI()
@app.post("/invoices/{id}/pdf")
async def render_invoice(id: int):
invoice = await Invoice.get(id)
async with httpx.AsyncClient(timeout=30) as client:
res = await client.post(
"https://api.transactional.dev/v1/generate",
json={
"documentId": os.environ["TRANSACTIONAL_INVOICE_TEMPLATE_UUID"],
"variables": invoice.to_template_vars(),
},
headers={"x-api-token": os.environ["TRANSACTIONAL_API_TOKEN"]},
)
if res.is_error:
body = res.json()
raise HTTPException(res.status_code, detail=body)
return res.json() # {url, documentId}