JSON PressBack to site

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"
}
FieldRequiredMeaning
templateyesthe v2 ReportDefinition; see template-format.md
data.rowsyesarray of row objects; values are string, number, boolean or null
paramsnostring values available to expressions; keys are case-insensitive
options.headersnoXLSX/CSV column labels; length must equal the detail-column count
options.includeNotesnoappend the note expression as a Notes column in XLSX/CSV
options.csvSafenoescape spreadsheet formula-leading CSV fields (default true); set false only for trusted machine-to-machine use
outputnopdf (default), docx, xlsx (alias excel) or csv

Rows are normalised before rendering:

Success

200 with the document bytes.

outputContent-TypeContent-Disposition
pdfapplication/pdfattachment; filename="<name>.pdf"
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentattachment; filename="<name>.docx"
xlsx/excelapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetattachment; filename="<name>.xlsx"
csvtext/csv; charset=utf-8attachment; 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.

StatusCodeWhen
400invalid_requestmalformed JSON, missing template/data.rows, or a template that does not match the v2 shape
400unsupported_template_versiontemplate.version is not 2
400invalid_rowsa row is not an object, keys differ only by case, or a value is an object/array
400invalid_optionsheader count mismatch or an unknown options key
400unsupported_outputoutput is not pdf, docx, xlsx, excel or csv
401unauthorizedmissing or unknown API key
408upload_timeoutthe complete request body did not arrive within JSONPRESS_UPLOAD_TIMEOUT_SECS
413payload_too_largerequest body exceeds JSONPRESS_MAX_BODY_MB
422render_failedunknown field, invalid geometry or expression error; details repeats the actionable message
422limit_exceededrows, pages, detail columns, output size or sparse-row expansion exceed their limits
429rate_limitedthe key exceeded JSONPRESS_RATE_PER_MINUTE; Retry-After: 60
503overloadedevery concurrent-render permit is in use; retry shortly
504render_timeoutthe 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.

LimitDefaultEnforced as
request body10 MiB413 payload_too_large
upload deadline10 s408 upload_timeout
rows per render50,000422 limit_exceeded
detail columns per render512422 limit_exceeded
widened row cells1,000,000 or four times input cells, whichever is greater422 limit_exceeded
widened row key bytes64 MiB or four times input key bytes, whichever is greater422 limit_exceeded
output file50 MiB422 limit_exceeded
pages per PDF1,000422 limit_exceeded
render deadline30 s504 render_timeout
concurrent rendersmax(1, cores - 1)503 overloaded
requests per key60/min429 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.