API reference
Base URL: http://localhost:5000 (the server binds 0.0.0.0:5000 by default).
The machine-readable contract is served at GET /openapi.json and checked in
as openapi.json.
Authentication
Every /v1 route requires a static API key:
Authorization: Bearer <secret>
Keys come from JSONPRESS_API_KEYS, a comma-separated name:secret list, for
example local:dev,ci:s3cret. An entry without a colon (for example dev) is
an unnamed key whose name is default; empty secrets are rejected at startup.
The scheme is case-insensitive. /healthz and /openapi.json are public and
never rate-limited.
GET /healthz
Public liveness check:
curl -sS http://localhost:5000/healthz
{"status":"ok","version":"0.1.0"}
GET /openapi.json
Public. Returns the checked-in OpenAPI 3.1 document with
Content-Type: application/json. The same file is compiled into the binary, so
the served contract cannot drift from a deployment.
GET /v1/fonts
Returns the PDF font families available to the designer. Requires the same Bearer API key as rendering; for example, with Noto Sans configured:
{"fonts":["Helvetica","Noto Sans"]}
Choose a report-wide family in the designer or set the template's font_family.
Text objects and detail columns may also select their own family; an omitted
override inherits the report font. PDF requires each selected family to be in
this catalog. DOCX names the family but does not embed it. Font paths are
configured on the server, never supplied by clients.
POST /v1/renders
Render one template with one set of rows.
Authorization: Bearer <secret>
Content-Type: application/json
Request body
{
"template": { "version": 2, "name": "Invoice", "...": "ReportDefinition" },
"data": {
"rows": [
{ "CUSTOMER": "Halden & Co.", "AMOUNT": "1250.00" },
{ "CUSTOMER": "Wren Supply", "AMOUNT": 89.5 }
]
},
"params": { "reportTitle": "Invoices" },
"options": { "headers": ["Customer", "Amount"], "includeNotes": false },
"output": "pdf"
}
| Field | Required | Meaning |
|---|---|---|
template | yes | the v2 ReportDefinition; see template-format.md |
data.rows | yes | array of row objects; values are string, number, boolean or null |
params | no | string values available to expressions; keys are case-insensitive |
options.headers | no | XLSX/CSV column labels; length must equal the detail-column count |
options.includeNotes | no | append the note expression as a Notes column in XLSX/CSV |
options.csvSafe | no | escape spreadsheet formula-leading CSV fields (default true); set false only for trusted machine-to-machine use |
output | no | pdf (default), docx, xlsx (alias excel) or csv |
Rows are normalised before rendering:
- Keys are field names and are matched case-insensitively, then uppercased.
- Two keys in one row that differ only by case are rejected as
invalid_rows. - Strings are kept verbatim; integers become decimal text; other numbers are
printed with the shortest round-trip representation (
1250.0→1250); booleans becometrue/false;nulland missing values become"". - Objects and arrays are not valid row values (
invalid_rows). - Rows may be sparse. The renderer takes the union of all keys and fills a
missing key with
""in that row. A field that appears in no row is unknown and fails the render (render_failed) if an expression reads it. - Send money as a string (
"1250.00") when the exact scale matters.
Success
200 with the document bytes.
output | Content-Type | Content-Disposition |
|---|---|---|
pdf | application/pdf | attachment; filename="<name>.pdf" |
docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document | attachment; filename="<name>.docx" |
xlsx/excel | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | attachment; filename="<name>.xlsx" |
csv | text/csv; charset=utf-8 | attachment; filename="<name>.csv" |
<name> is the template name lowercased, with every run of characters that
are not ASCII letters or digits replaced by -; leading and trailing runs are
dropped, and an empty result becomes report (for example " a/b/c " becomes
a-b-c).
DOCX is an editable, flowing Word document rather than a pixel-identical PDF layout. Title objects appear in the repeating header, detail rows in a Word table, and summary objects after the detail. Word controls pagination. Fonts are named in the document, not embedded, and must be installed on the viewer's system. Page and total-page expressions are supported as live Word fields when they are plain concatenations with quoted text; more complex page-dependent expressions return an error instead of an incorrect value.
XLSX and CSV use the detail-column labels (taken from the title band, or
options.headers when supplied). PDF draws the page bands; XLSX writes the
title, detail rows and summary as spreadsheet rows; CSV is a flat record
export, so it omits the summary band. When a column has no label, XLSX writes
Column N (for example Column 3) and CSV writes an empty header cell. By
default, CSV fields that could be interpreted as spreadsheet formulas are
prefixed with an apostrophe. For trusted machine-to-machine consumers that need
the original values, set options.csvSafe to false; raw CSV must not be
opened in a spreadsheet when values are untrusted.
When a CSV template contains fixed table objects, the response has an
X-JSONPress-Warning header explaining that those tables were omitted.
The designer also displays this warning after download.
Example
The neutral fixture is a complete request template:
jq -n --slurpfile template crates/jsonpress-core/testdata/invoice.json \
'{template: $template[0],
data: {rows: [{CUSTOMER: "Halden & Co.", AMOUNT: "1250.00"},
{CUSTOMER: "Wren Supply", AMOUNT: "89.50"}]},
output: "pdf"}' |
curl -sS http://localhost:5000/v1/renders \
-H 'Authorization: Bearer dev' \
-H 'Content-Type: application/json' \
--data-binary @- \
-o invoice.pdf
Replace output with "csv" to see the two-column table as text.
Errors
Every failure uses the same JSON envelope, with one exception: a request that
uses the wrong HTTP method on a registered route returns axum's plain
405 Method Not Allowed rather than this envelope.
{
"error": {
"code": "invalid_rows",
"message": "row 2 must be an object",
"details": []
}
}
details is always an array; it carries extra context such as the two
case-colliding field names.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | malformed JSON, missing template/data.rows, or a template that does not match the v2 shape |
| 400 | unsupported_template_version | template.version is not 2 |
| 400 | invalid_rows | a row is not an object, keys differ only by case, or a value is an object/array |
| 400 | invalid_options | header count mismatch or an unknown options key |
| 400 | unsupported_output | output is not pdf, docx, xlsx, excel or csv |
| 401 | unauthorized | missing or unknown API key |
| 408 | upload_timeout | the complete request body did not arrive within JSONPRESS_UPLOAD_TIMEOUT_SECS |
| 413 | payload_too_large | request body exceeds JSONPRESS_MAX_BODY_MB |
| 422 | render_failed | unknown field, invalid geometry or expression error; details repeats the actionable message |
| 422 | limit_exceeded | rows, pages, detail columns, output size or sparse-row expansion exceed their limits |
| 429 | rate_limited | the key exceeded JSONPRESS_RATE_PER_MINUTE; Retry-After: 60 |
| 503 | overloaded | every concurrent-render permit is in use; retry shortly |
| 504 | render_timeout | the render exceeded JSONPRESS_TIMEOUT_SECS |
Unknown /v1 paths return a JSON 404 with {"error":{"code":"not_found",...}}.
An unbuilt or missing designer directory returns designer_missing; neither is
part of the render contract above.
Render failures are actionable: the message names the expression and the unknown field or the invalid geometry rather than returning a bare internal error.
Limits
All defaults are configurable with environment variables; see deploy.md.
| Limit | Default | Enforced as |
|---|---|---|
| request body | 10 MiB | 413 payload_too_large |
| upload deadline | 10 s | 408 upload_timeout |
| rows per render | 50,000 | 422 limit_exceeded |
| detail columns per render | 512 | 422 limit_exceeded |
| widened row cells | 1,000,000 or four times input cells, whichever is greater | 422 limit_exceeded |
| widened row key bytes | 64 MiB or four times input key bytes, whichever is greater | 422 limit_exceeded |
| output file | 50 MiB | 422 limit_exceeded |
| pages per PDF | 1,000 | 422 limit_exceeded |
| render deadline | 30 s | 504 render_timeout |
| concurrent renders | max(1, cores - 1) | 503 overloaded |
| requests per key | 60/min | 429 rate_limited |
For DOCX, the page limit uses a conservative lower bound based on total minimum row height and physical page height, not the PDF's first-row position. The final page count depends on the Word-compatible application that lays out the file.
For PDF and XLSX, accumulated detail and note text is also limited to 16 times the output cap (at least 1 MiB); DOCX applies that budget to its uncompressed detail table. This limits intermediate memory even for highly compressible data. An admission slot remains occupied while output bytes are in transit.
The rate limiter is a fixed one-minute window per key, anchored at the key's first request. Authentication runs before the limiter, so a rejected key never spends a limiter token, and health/openapi requests are not counted.