api/pdf (PDF service)
Generated from api/pdf/README.md
PDF renderer for Ulams certificates and PDF templates, built on
pdfme (@pdfme/generator, @pdfme/schemas, MIT). Laravel
(api/packages/templates-pdf) is its only client; browsers never call it.
Licence: MIT (LICENSE). Bundled fonts: SIL OFL 1.1 (fonts/README.md).
All routes except /health require X-Internal-Token: $PDF_INTERNAL_TOKEN.
| Route | Answer |
|---|---|
GET /health |
{"status":"ok","active":0,"fonts":9} |
POST /render |
body {"template": <pdfme template>, "inputs": [{"<field name>": "<value>"}, ...]} → application/pdf (one copy of the template per input record) |
GET /fonts |
{"data":[{"name":"NotoSans-Regular","file":"NotoSans-Regular.ttf","fallback":true}, ...]} |
GET /fonts/:file |
TTF bytes |
/render behaviour:
inputsdefaults to[{}]. Every editable field gets a string; missing values render empty (headerX-Render-Warnings: <n>), barcode/QR fields with an empty value are left out. Fields markedrequiredwithout a value →422 missing_required_fieldswith the field names.- Only the bundled fonts are used; an unknown
fontNamefalls back to Noto Sans. - No outbound requests:
basePdfmust be a blank page ({width, height, padding}) or adata:application/pdf;base64,URI, images must bedata:URIs. - Errors are JSON
{"error": "<code>", "message": "...", "details": ...}: 400invalid_template/invalid_inputs/invalid_json, 401unauthorized, 413payload_too_large/too_many_inputs/too_many_pages/too_many_fields, 422unsupported_field_type/remote_image/missing_required_fields/render_failed, 503busy(withRetry-After), 504render_timeout.
Field types: text, multi-variable text, image, SVG, line, rectangle, ellipse, table, QR code,
Code128, Code39, EAN-13, EAN-8, PDF417, DataMatrix (src/plugins.ts, mirrored by the admin
designer in admin/src/components/PdfEditor/plugins.ts).
Limits and configuration
Section titled “Limits and configuration”pdfme runs in worker threads; a render over the time limit terminates its worker, which is replaced.
| Env | Default | |
|---|---|---|
PORT |
3000 |
|
PDF_INTERNAL_TOKEN |
— | required; the service refuses to start without it |
PDF_MAX_BODY_SIZE |
5mb |
JSON body limit |
PDF_MAX_INPUTS |
100 |
input records per request |
PDF_MAX_PAGES / PDF_MAX_FIELDS_PER_PAGE |
20 / 200 |
|
PDF_RENDER_TIMEOUT_MS |
20000 |
|
PDF_MAX_CONCURRENCY / PDF_MAX_QUEUE |
2 / 16 |
worker threads / waiting renders before 503 |
PDF_FONTS_DIR |
./fonts |
|
LOG_LEVEL |
info |
pino |
Development
Section titled “Development”corepack yarn workspace api-pdf test # vitest (Node 22)corepack yarn workspace api-pdf dev # PDF_INTERNAL_TOKEN=... requireddocker compose -f api/docker-compose.yml up -d --build pdfnode api/pdf/scripts/certificate-templates.mjs # rebuild templates-pdf/resources/pdfme/certificate-*.jsonThe image is built from the repository root (api/pdf/Dockerfile, allow-list in
Dockerfile.dockerignore), like api/h5p.