{"openapi":"3.1.0","info":{"title":"Wathiq Render API","version":"0.7.0","description":"Document generation API: templates as data (the Wathiq IR), pixel-perfect\nPDFs out. Deterministic rendering on a pinned engine; failures are always\nerrors — never empty bytes.\n\n**Authentication** — when the server is started with `WATHIQ_API_KEYS`,\nevery endpoint except `GET /health` and the Studio requires an\n`X-API-Key` header.\n"},"servers":[{"url":"http://localhost:8080"}],"tags":[{"name":"render"},{"name":"jobs"},{"name":"templates"},{"name":"quality"},{"name":"system"}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key"}},"schemas":{"Template":{"type":"object","description":"A Wathiq IR template. Full JSON Schema at packages/ir/schema/ir.schema.json.","required":["ir","page","body"],"properties":{"ir":{"type":"string","example":"0.1.0"},"page":{"type":"object"},"styles":{"type":"object"},"masters":{"type":"object"},"components":{"type":"object"},"watermark":{"type":"object"},"body":{"type":"array","items":{"type":"object"}}}},"RenderRequest":{"type":"object","properties":{"template":{"$ref":"#/components/schemas/Template"},"templateRef":{"type":"string","description":"Registry reference — \"name\" (latest) or \"name@1.2.0\". Alternative to inline template.","example":"invoice@1.2.0"},"data":{"description":"The document data payload (any JSON)."},"profile":{"type":"string","enum":["pdf","pdf/a-2b","pdf/ua-1"],"description":"pdf/ua-1 = tagged, accessible PDF (screen-reader navigable — WCAG/Section-508 procurement), typst engine only (auto-routed). With engine=chromium, pdf/a-2b requires Ghostscript on the server; with engine=typst it is produced natively (--pdf-standard a-2b)."},"format":{"type":"string","enum":["pdf","docx"],"default":"pdf","description":"docx renders the same IR to an editable Word document (real tables, headers/footers, native page-number fields, RTL); engine/profile apply to pdf only. Word reflows text with its own engine, so pagination is not pixel-identical to the PDF."},"engine":{"type":"string","enum":["chromium","typst"],"default":"chromium","description":"Render engine. chromium is the reference renderer; typst is the Phase-4 high-speed backend (requires the typst binary on the server — see GET /health). Layout parity is tracked per template by the golden harness; responses carry X-Wathiq-Engine."}},"description":"Exactly one of template / templateRef is required."},"JobStatus":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","succeeded","failed"]},"createdAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"},"error":{"type":"string"},"bytes":{"type":"integer"},"resultUrl":{"type":"string"}}},"LintReport":{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["error","warning"]},"kind":{"type":"string","enum":["UNCOVERED","FALLBACK","UNKNOWN_FONT"]},"style":{"type":"string"},"chain":{"type":"array","items":{"type":"string"}},"chars":{"type":"array","items":{"type":"object"}},"message":{"type":"string"}}}},"checkedChars":{"type":"integer"},"errors":{"type":"integer"},"warnings":{"type":"integer"}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object"}}}}}},"security":[{"apiKey":[]}],"paths":{"/health":{"get":{"tags":["system"],"summary":"Liveness and engine info","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"engine":{"type":"string"},"typst":{"type":"string","description":"typst version","or \"unavailable\"":null},"ir":{"type":"string"}}}}}}}}},"/render":{"post":{"tags":["render"],"summary":"Render a PDF synchronously","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"}}}},"responses":{"200":{"description":"The rendered PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Missing template/templateRef","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid X-API-Key"},"404":{"description":"templateRef not found in the registry"},"422":{"description":"Template failed IR validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Render failed (a failed render is never a 200)"}}}},"/sign":{"post":{"tags":["render"],"summary":"Apply the organization's digital signature (seal) to a PDF","description":"Body: raw PDF bytes. Returns the same PDF carrying a cryptographic CMS/PKCS#7 signature (SubFilter adbe.pkcs7.detached) made with the server's certificate (WATHIQ_SIGN_P12 / WATHIQ_SIGN_P12_PASSWORD). Validators (Adobe Reader, pyHanko, DSS) verify byte integrity and the signer chain. Distinct from signature FIELDS, which collect people's ink signatures — this is the issuing organization's seal. /render also accepts \"sign\": true | {reason, location, contactInfo, name} to render and seal in one call.\n","parameters":[{"name":"reason","in":"query","schema":{"type":"string"}},{"name":"location","in":"query","schema":{"type":"string"}},{"name":"contactInfo","in":"query","schema":{"type":"string"}},{"name":"name","in":"query","schema":{"type":"string"},"description":"signer name on the stamp (default cert CN)"},{"name":"visible","in":"query","schema":{"type":"string"},"description":"printed stamp: \"1\" (last page, bottom-right) or \"<page|first|last>:<corner>\", e.g. \"last:bottom-left\""}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"The digitally signed PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Body is not a PDF"},"503":{"description":"Signing not configured (WATHIQ_SIGN_P12 unset)"}}}},"/sign/status":{"get":{"tags":["render"],"summary":"Whether a signing certificate is configured","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"configured":{"type":"boolean"}}}}}}}}},"/render-batch":{"post":{"tags":["render"],"summary":"Render one template against many data rows — returns a zip","description":"JSON body: { template | templateRef, items: object[], engine?, format?, filenameBind? } — filenameBind is an expression evaluated per item to name each file (e.g. \"$.invoiceNumber\"). Alternatively send text/csv with ?templateRef= — the header row becomes field names and numeric cells are auto-cast. A row that fails renders an <name>.error.txt entry instead of failing the whole batch. Response headers x-wathiq-batch-total / x-wathiq-batch-failed summarise the run. Max items: WATHIQ_BATCH_MAX (default 500).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"template":{"type":"object"},"templateRef":{"type":"string"},"items":{"type":"array","items":{"type":"object"}},"engine":{"type":"string","enum":["chromium","typst"]},"format":{"type":"string","enum":["pdf","docx"],"default":"pdf"},"filenameBind":{"type":"string"}}}},"text/csv":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"Zip of rendered documents","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Missing template/items","batch too large":null,"or bad format":null},"422":{"description":"Template failed IR validation"}}}},"/import":{"post":{"tags":["authoring"],"summary":"Convert an existing DOCX/PDF into a draft template","description":"Send the raw file bytes. Returns a draft template (valid IR), sample data extracted from the document (e.g. table rows), review notes, and suggested data bindings for fragments that look like data (dates, amounts, emails, references). The file kind is inferred from the magic bytes when ?kind is omitted.\n","parameters":[{"name":"kind","in":"query","schema":{"type":"string","enum":["docx","pdf"]}},{"name":"name","in":"query","schema":{"type":"string"},"description":"template name for the draft"}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Draft template + data + notes + binding suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"template":{"type":"object"},"data":{"type":"object"},"notes":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"message":{"type":"string"}}}},"suggestions":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"dotted block path","e.g. \"2\" or \"2.1\"":null},"fragment":{"type":"string"},"field":{"type":"string"},"reason":{"type":"string","enum":["date","amount","email","phone","id","number"]}}}}}}}}},"400":{"description":"Empty body or unsupported file type"},"422":{"description":"The file could not be parsed"}}}},"/validate":{"post":{"tags":["quality"],"summary":"Validate a template against the IR schema","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["template"],"properties":{"template":{"$ref":"#/components/schemas/Template"}}}}}},"responses":{"200":{"description":"Validation report","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"errors":{"type":"array","items":{"type":"object"}}}}}}}}}},"/lint":{"post":{"tags":["quality"],"summary":"Glyph-coverage lint — find tofu before it renders","description":"Checks every character the render will produce against the actual font files in each style's chain.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"}}}},"responses":{"200":{"description":"Lint report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LintReport"}}}},"422":{"description":"Template failed IR validation"}}}},"/render-jobs":{"post":{"tags":["jobs"],"summary":"Enqueue an async render","description":"Returns 202 immediately. With `WATHIQ_JOB_STORE=file`, jobs survive\nrestarts (interrupted jobs re-run automatically). Optional\n`webhookUrl` receives a POST on completion (3 attempts, best-effort).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/RenderRequest"},{"type":"object","properties":{"webhookUrl":{"type":"string","format":"uri"},"keepResult":{"type":"boolean","description":"Keep the PDF downloadable after the first GET (default false = single download)."}}}]}}}},"responses":{"202":{"description":"Job accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"503":{"description":"Job store full"}}}},"/render-jobs/{id}":{"get":{"tags":["jobs"],"summary":"Job status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Status document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"404":{"description":"Unknown job"}}}},"/render-jobs/{id}/pdf":{"get":{"tags":["jobs"],"summary":"Download the job result","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Unknown job"},"410":{"description":"Job failed","or result already downloaded / expired":null},"425":{"description":"Job not finished yet"}}}},"/templates":{"get":{"tags":["templates"],"summary":"List registry templates","responses":{"200":{"description":"All templates with their versions","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"latest":{"type":"string"},"versions":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}}}}}}}}}},"/templates/{name}":{"get":{"tags":["templates"],"summary":"Get the latest version of a template","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ name, version, template }"},"404":{"description":"Not found"}}},"post":{"tags":["templates"],"summary":"Publish a template version (immutable)","description":"Without `version`, the latest is bumped (`bump`: patch default,\nminor, major; first publish is 1.0.0). Explicit versions must be\nnew and higher than latest — published versions never change.\n","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-_]{0,63}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["template"],"properties":{"template":{"$ref":"#/components/schemas/Template"},"version":{"type":"string","example":"2.0.0"},"bump":{"type":"string","enum":["major","minor","patch"]}}}}}},"responses":{"201":{"description":"{ name, version }"},"409":{"description":"Version exists or is not higher than latest"},"422":{"description":"Template failed IR validation"}}}},"/templates/{name}/{version}":{"get":{"tags":["templates"],"summary":"Get an exact template version","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ name, version, template }"},"404":{"description":"Not found"}}},"delete":{"tags":["templates"],"summary":"Delete a template version","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}}}}}}