{
  "openapi": "3.1.0",
  "info": {
    "title": "JSON Press API",
    "version": "0.1.0",
    "summary": "Render a report definition to PDF, DOCX, XLSX or CSV.",
    "description": "JSON Press is a self-hosted, stateless report renderer. Every request carries its own template and rows; the server keeps no database and makes no outbound network calls.\n\n## Authentication\n\n`POST /v1/renders` requires `Authorization: Bearer <api-key>`. Keys are configured with `JSONPRESS_API_KEYS` as a comma-separated `name:secret` list.\n\n## Errors\n\nEvery failure uses the same JSON envelope:\n\n```json\n{\"error\":{\"code\":\"invalid_rows\",\"message\":\"row 2 must be an object\",\"details\":[]}}\n```\n\n| Status | Code | When |\n|---|---|---|\n| 400 | `invalid_request` | malformed JSON, a missing `template`/`data.rows`, or a template that does not match the v2 shape |\n| 400 | `unsupported_template_version` | `template.version` is not `2` |\n| 400 | `invalid_rows` | a row is not an object, keys differ only by case, or a value is an object/array |\n| 400 | `invalid_options` | header count mismatch or an unknown option |\n| 400 | `unsupported_output` | `output` is not `pdf`, `docx`, `xlsx`, `excel` or `csv` |\n| 401 | `unauthorized` | missing or unknown API key |\n| 408 | `upload_timeout` | request body did not arrive before the upload deadline |\n| 413 | `payload_too_large` | request body exceeds `JSONPRESS_MAX_BODY_MB` |\n| 422 | `render_failed` | unknown field, invalid geometry or expression error |\n| 422 | `limit_exceeded` | rows, pages, detail columns or sparse-row expansion exceed the cap |\n| 429 | `rate_limited` | the key exceeded `JSONPRESS_RATE_PER_MINUTE` |\n| 503 | `overloaded` | no concurrent-render permit was free |\n| 504 | `render_timeout` | the render exceeded `JSONPRESS_TIMEOUT_SECS` |"
  },
  "servers": [
    {
      "url": "http://localhost:5000",
      "description": "Local docker run / bare server"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Render",
      "description": "Create a document from a template and rows."
    },
    {
      "name": "Service",
      "description": "Health and API description."
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "tags": ["Service"],
        "operationId": "getHealth",
        "summary": "Liveness and version",
        "description": "Public. Never rate-limited and needs no API key.",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "version": "0.1.0"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": ["Service"],
        "operationId": "getOpenapi",
        "summary": "This OpenAPI document",
        "description": "Public. Serves the checked-in OpenAPI 3.1 contract with `Content-Type: application/json`.",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.1 document."
                }
              }
            }
          }
        }
      }
    },
    "/v1/fonts": {
      "get": {
        "tags": ["Service"],
        "operationId": "listFonts",
        "summary": "List available PDF font families",
        "description": "Returns family names only; font paths remain server-side.",
        "security": [{"bearerAuth": []}],
        "responses": {
          "200": {
            "description": "Available PDF font family names.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["fonts"],
                  "properties": {
                    "fonts": {"type": "array", "items": {"type": "string"}}
                  }
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or does not match.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ErrorEnvelope"}
              }
            }
          },
          "429": {
            "description": "The API key exceeded its request rate.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ErrorEnvelope"}
              }
            }
          }
        }
      }
    },
    "/v1/renders": {
      "post": {
        "tags": ["Render"],
        "operationId": "renderReport",
        "summary": "Render a template to PDF, DOCX, XLSX or CSV",
        "description": "Validates the request, renders with `jsonpress-core`, and returns the document as a download. The operation is stateless: the same request always renders the same rows, apart from date/time expressions that use the server clock.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rendered document. `Content-Disposition` names the download `<slugged-template-name>.<extension>`.",
            "headers": {
              "Content-Disposition": {
                "description": "Attachment header, for example `attachment; filename=\"invoice.pdf\"`.",
                "schema": {
                  "type": "string"
                }
              },
              "X-JSONPress-Warning": {
                "description": "Present for CSV when fixed tables were omitted.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/csv; charset=utf-8": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`, `unsupported_template_version`, `invalid_rows`, `invalid_options` or `unsupported_output`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`: the API key is missing or does not match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "408": {
            "description": "`upload_timeout`: the request body did not arrive within the configured upload deadline.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large`: the request body exceeds the configured limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "`render_failed` (unknown field, invalid geometry, expression error) or `limit_exceeded` (rows, pages, detail columns or sparse-row expansion over the cap).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: the key exceeded its per-minute allowance. `Retry-After` is 60 seconds.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the fixed window restarts.",
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "`overloaded`: every concurrent-render permit is in use; retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "504": {
            "description": "`render_timeout`: the render exceeded the configured deadline.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from `JSONPRESS_API_KEYS`."
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "required": ["status", "version"],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "version": {
            "type": "string",
            "example": "0.1.0"
          }
        }
      },
      "RenderRequest": {
        "type": "object",
        "required": ["template", "data"],
        "properties": {
          "template": {
            "$ref": "#/components/schemas/ReportDefinition"
          },
          "data": {
            "type": "object",
            "required": ["rows"],
            "properties": {
              "rows": {
                "type": "array",
                "description": "One object per detail row. Keys are field names, matched case-insensitively. Rows may be sparse; missing fields render as an empty string.",
                "items": {
                  "$ref": "#/components/schemas/Row"
                }
              }
            }
          },
          "params": {
            "type": "object",
            "description": "Optional string values available to expressions. Keys are matched case-insensitively.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "options": {
            "type": "object",
            "description": "Optional export settings. Unknown keys are rejected as `invalid_options`.",
            "additionalProperties": false,
            "properties": {
              "headers": {
                "type": "array",
                "description": "Override the XLSX/CSV column labels. The length must equal the number of detail columns.",
                "items": {
                  "type": "string"
                }
              },
              "includeNotes": {
                "type": "boolean",
                "default": false,
                "description": "Append the note expression as a `Notes` column in XLSX and CSV."
              },
              "csvSafe": {
                "type": "boolean",
                "default": true,
                "description": "Escape spreadsheet formula-leading CSV fields. Set false only for trusted machine-to-machine consumers that require raw values."
              }
            }
          },
          "output": {
            "type": "string",
            "description": "Output format. Omitted means `pdf`.",
            "enum": ["pdf", "docx", "xlsx", "excel", "csv"],
            "default": "pdf"
          }
        }
      },
      "Row": {
        "type": "object",
        "description": "A row of field values. At least one key is normally present; a row with no keys renders blanks for every column.",
        "additionalProperties": {
          "$ref": "#/components/schemas/RowValue"
        }
      },
      "RowValue": {
        "description": "A scalar value. Objects and arrays are rejected as `invalid_rows`.",
        "type": ["string", "number", "boolean", "null"]
      },
      "ReportDefinition": {
        "type": "object",
        "description": "The v2 template. Keys are snake_case. `fields`, `sample_rows` and `parameters` are additive, design-time metadata and are ignored by the render path.",
        "required": ["version", "name", "page", "title", "detail", "summary"],
        "properties": {
          "version": {
            "type": "integer",
            "const": 2
          },
          "name": {
            "type": "string",
            "description": "Used to build the download file name.",
            "example": "Invoice"
          },
          "source": {
            "type": "string",
            "default": "",
            "description": "Optional audit trail naming the original template file."
          },
          "font_family": {
            "type": ["string", "null"],
            "description": "PDF font family from GET /v1/fonts. Omit or set null to use the server default. Unknown family names fail PDF rendering."
          },
          "page": {
            "$ref": "#/components/schemas/PageSpec"
          },
          "title": {
            "type": "array",
            "description": "Printed on every page, in page coordinates.",
            "items": {
              "$ref": "#/components/schemas/Element"
            }
          },
          "detail": {
            "$ref": "#/components/schemas/DetailBand"
          },
          "note": {
            "$ref": "#/components/schemas/NoteSpec"
          },
          "summary": {
            "type": "array",
            "description": "Printed after the last detail row. Text `y_mm` is relative to the top of the summary block.",
            "items": {
              "$ref": "#/components/schemas/Element"
            }
          },
          "fields": {
            "type": "array",
            "description": "Designer field catalog. Ignored by the render path.",
            "items": {
              "$ref": "#/components/schemas/FieldSpec"
            }
          },
          "sample_rows": {
            "type": "array",
            "description": "Designer preview rows. Ignored by the render path; a render uses `data.rows` only. The key is snake_case, not `sampleRows`.",
            "items": {
              "type": "object",
              "additionalProperties": {
                "$ref": "#/components/schemas/RowValue"
              }
            }
          },
          "parameters": {
            "type": "array",
            "description": "Designer parameter form. Ignored by the render path; request values arrive in `params`.",
            "items": {
              "$ref": "#/components/schemas/ParamSpec"
            }
          }
        }
      },
      "PageSpec": {
        "type": "object",
        "required": ["width_mm", "height_mm", "bottom_margin_mm"],
        "properties": {
          "width_mm": {
            "type": "number",
            "example": 210
          },
          "height_mm": {
            "type": "number",
            "example": 297
          },
          "top_margin_mm": {
            "type": "number",
            "description": "Top print-area guide; zero keeps the page edge.",
            "example": 0
          },
          "left_margin_mm": {
            "type": "number",
            "description": "Left print-area guide; zero keeps the page edge.",
            "example": 0
          },
          "right_margin_mm": {
            "type": "number",
            "description": "Right print-area guide; zero keeps the page edge.",
            "example": 0
          },
          "bottom_margin_mm": {
            "type": "number",
            "description": "Bottom print limit; the flow engine stops adding rows past it.",
            "example": 15
          }
        }
      },
      "Element": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/TextElement"
          },
          {
            "$ref": "#/components/schemas/LineElement"
          },
          {
            "$ref": "#/components/schemas/RectElement"
          },
          {
            "$ref": "#/components/schemas/TableElement"
          },
          {
            "$ref": "#/components/schemas/ImageElement"
          }
        ],
        "discriminator": {
          "propertyName": "kind",
          "mapping": {
            "text": "#/components/schemas/TextElement",
            "line": "#/components/schemas/LineElement",
            "rect": "#/components/schemas/RectElement",
            "table": "#/components/schemas/TableElement",
            "image": "#/components/schemas/ImageElement"
          }
        }
      },
      "ImageElement": {
        "type": "object",
        "required": ["kind", "x0_mm", "y0_mm", "width_mm", "height_mm", "src"],
        "properties": {
          "kind": { "type": "string", "const": "image" },
          "x0_mm": { "type": "number" },
          "y0_mm": { "type": "number" },
          "width_mm": { "type": "number", "exclusiveMinimum": 0 },
          "height_mm": { "type": "number", "exclusiveMinimum": 0 },
          "src": {
            "type": "string",
            "pattern": "^data:image/(png|jpeg);base64,",
            "description": "A fixed PNG or JPEG embedded as a base64 data URL; no external references are fetched."
          }
        }
      },
      "TableElement": {
        "type": "object",
        "required": ["kind", "x0_mm", "y0_mm", "width_mm", "height_mm", "size_pt", "cells"],
        "properties": {
          "kind": { "type": "string", "const": "table" },
          "x0_mm": { "type": "number" },
          "y0_mm": { "type": "number" },
          "width_mm": { "type": "number", "exclusiveMinimum": 0 },
          "height_mm": { "type": "number", "exclusiveMinimum": 0 },
          "size_pt": { "type": "number", "exclusiveMinimum": 0 },
          "border": { "type": "string", "enum": ["solid", "dashed", "dotted", "none"], "default": "solid" },
          "column_weights": { "type": "array", "items": { "type": "number", "exclusiveMinimum": 0 }, "description": "Relative column widths; omit or use an empty array for equal widths. Otherwise supply one weight per column." },
          "row_weights": { "type": "array", "items": { "type": "number", "exclusiveMinimum": 0 }, "description": "Relative row heights; omit or use an empty array for equal heights. Otherwise supply one weight per row." },
          "cells": { "type": "array", "minItems": 1, "maxItems": 50, "items": {
            "type": "array", "minItems": 1, "maxItems": 50, "items": {
              "oneOf": [
                { "type": "string", "description": "Expression, left-aligned." },
                {
                  "type": "object",
                  "required": ["expr"],
                  "properties": {
                    "expr": { "type": "string" },
                    "align": { "type": "string", "enum": ["left", "center", "right"], "default": "left" }
                  }
                }
              ]
            }
          } }
        }
      },
      "TextElement": {
        "type": "object",
        "required": ["kind", "x_mm", "y_mm", "width_mm", "size_pt", "expr"],
        "properties": {
          "kind": {
            "type": "string",
            "const": "text"
          },
          "x_mm": {
            "type": "number"
          },
          "y_mm": {
            "type": "number",
            "description": "Baseline in top-origin millimetres."
          },
          "width_mm": {
            "type": "number"
          },
          "size_pt": {
            "type": "number"
          },
          "bold": {
            "type": "boolean",
            "default": false
          },
          "align": {
            "$ref": "#/components/schemas/Align"
          },
          "color": {
            "$ref": "#/components/schemas/ColorRole"
          },
          "expr": {
            "type": "string",
            "description": "Expression. A quoted literal such as `\"Invoice\"` prints fixed text."
          },
          "total_type": {
            "type": "integer",
            "enum": [0, 1, 2],
            "default": 0,
            "description": "In a summary band: 0 = no aggregate, 1 = count, 2 = sum."
          }
        }
      },
      "LineElement": {
        "type": "object",
        "required": ["kind", "x0_mm", "y0_mm", "x1_mm", "y1_mm", "thickness_mm"],
        "properties": {
          "kind": {
            "type": "string",
            "const": "line"
          },
          "x0_mm": {
            "type": "number"
          },
          "y0_mm": {
            "type": "number"
          },
          "x1_mm": {
            "type": "number"
          },
          "y1_mm": {
            "type": "number"
          },
          "thickness_mm": {
            "type": "number"
          }
        }
      },
      "RectElement": {
        "type": "object",
        "required": ["kind", "x0_mm", "y0_mm", "width_mm", "height_mm"],
        "properties": {
          "kind": {
            "type": "string",
            "const": "rect"
          },
          "x0_mm": {
            "type": "number"
          },
          "y0_mm": {
            "type": "number"
          },
          "width_mm": {
            "type": "number"
          },
          "height_mm": {
            "type": "number"
          },
          "filled": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "Align": {
        "type": "string",
        "enum": ["left", "right"],
        "default": "left"
      },
      "ColorRole": {
        "type": "string",
        "enum": ["default", "title", "muted"],
        "default": "default"
      },
      "DetailBand": {
        "type": "object",
        "required": ["row_height_mm", "first_row_y_mm", "columns"],
        "properties": {
          "row_height_mm": {
            "type": "number",
            "description": "Band height and therefore the pitch between rows."
          },
          "first_row_y_mm": {
            "type": "number",
            "description": "First row baseline on a page."
          },
          "columns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetailColumn"
            }
          },
          "elements": {
            "type": "array",
            "description": "Objects repeated once per data row; y values are relative to the row baseline.",
            "items": {
              "$ref": "#/components/schemas/Element"
            }
          }
        }
      },
      "DetailColumn": {
        "type": "object",
        "required": ["x_mm", "width_mm", "size_pt", "expr"],
        "properties": {
          "x_mm": {
            "type": "number"
          },
          "width_mm": {
            "type": "number"
          },
          "size_pt": {
            "type": "number"
          },
          "bold": {
            "type": "boolean",
            "default": false
          },
          "align": {
            "$ref": "#/components/schemas/Align"
          },
          "stretch": {
            "type": "boolean",
            "default": false,
            "description": "Wrap long values instead of clipping them."
          },
          "label": {
            "type": "string",
            "description": "Header label. The server fills this from the nearest title-band header when omitted."
          },
          "expr": {
            "type": "string"
          }
        }
      },
      "NoteSpec": {
        "type": "object",
        "required": [
          "x_mm",
          "width_mm",
          "size_pt",
          "offset_mm",
          "extra_mm",
          "expr"
        ],
        "properties": {
          "x_mm": {
            "type": "number"
          },
          "width_mm": {
            "type": "number"
          },
          "size_pt": {
            "type": "number"
          },
          "offset_mm": {
            "type": "number",
            "description": "Distance from the row baseline to the note baseline."
          },
          "extra_mm": {
            "type": "number",
            "description": "Extra flow height consumed when a note prints."
          },
          "expr": {
            "type": "string"
          }
        }
      },
      "FieldSpec": {
        "type": "object",
        "required": ["name", "label"],
        "properties": {
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "sample": {
            "type": "string",
            "default": ""
          }
        }
      },
      "ParamSpec": {
        "type": "object",
        "required": ["name", "label", "kind", "default"],
        "properties": {
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/ParamKind"
          },
          "default": {
            "description": "Default value; its JSON type follows `kind`."
          },
          "options": {
            "type": "array",
            "default": [],
            "items": {
              "$ref": "#/components/schemas/ParamOption"
            }
          }
        }
      },
      "ParamKind": {
        "type": "string",
        "enum": ["text", "integer", "boolean", "select"]
      },
      "ParamOption": {
        "type": "object",
        "required": ["value", "label"],
        "properties": {
          "value": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail"
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "required": ["code", "message", "details"],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string",
            "description": "One-line, human-readable explanation."
          },
          "details": {
            "type": "array",
            "description": "Additional machine-readable context, often the colliding field names.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "invalid_request",
          "unsupported_template_version",
          "invalid_rows",
          "invalid_options",
          "unsupported_output",
          "unauthorized",
          "payload_too_large",
          "render_failed",
          "limit_exceeded",
          "rate_limited",
          "overloaded",
          "render_timeout"
        ]
      }
    }
  }
}
