Rendering
Endpoints that turn a Template into output. Two of them stream a rendered document and one resolves typed render parameters against a phase context.
Rendering runs through a fixed pre-block pipeline: look up the Template, resolve
its input parameters, run its processors to fetch data, build the render
context, then hand off to the output handler that produces the final format.
Supported output formats include pdf, html, docx, xlsx, and text; the
handler defaults to the Template's own handler unless the request overrides it.
GET /api/templates/{id}/_render
Render a Template using query parameters. The {id} is the Template's natural
id (ownerId:slug), resolved through the read_access scope.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), template.retrievePayload, template.resolveParams, template.processInputs, template.context, template.handler(query.output)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
output | string | Output format (pdf, html, docx, xlsx, text); overrides the Template's default handler |
progress | string | Progress identifier for tracking a long-running render |
download | boolean | When set, sends the response as an attachment |
filename | string | Custom download filename (defaults to {slug}.{output}) |
payloadId | string | Id of a payload stored by a prior render, retrieved by template.retrievePayload |
params | object | Input parameter values |
Unknown query parameters are allowed, so input-specific values can be passed directly alongside the parameters above.
Example Request:
GET /api/templates/admin:sales-order-report/_render?output=pdf&download=true&filename=report.pdf¶ms[shipState]=NY
Response:
Returns the rendered document with the Content-Type for the chosen output
format. When download is set, a Content-Disposition: attachment header is
added with filename (or the Template slug). The socket timeout is raised to 60
minutes to accommodate long-running renders.
When a draft Template's processors fail during a render, the response is an HTML error page listing the processor errors instead of an HTTP error. A published Template surfaces the failure as a normal error response.
The response body is a binary document (PDF/DOCX/XLSX) or a host-specific
HTML/text string, so it is not captured as a docs fixture here. This endpoint is
exercised by tests/api/templates/{id}/_render/route-spec.js.
POST /api/templates/{id}/_render
Render a Template using a request body for parameters and render options. Unlike
the GET route, POST does not accept a payloadId (it has no
template.retrievePayload pre-block).
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), template.resolveParams, template.processInputs, template.context, template.handler
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
output | string | Output format (can also be set in the body) |
progress | string | Progress identifier |
download | boolean | Send as attachment (can also be set in the body) |
filename | string | Custom download filename (can also be set in the body) |
params | object | Input parameter values (can also be set in the body) |
Request Body:
| Field | Type | Description |
|---|---|---|
output | string | Output format override |
progress | string | Progress identifier |
download | boolean | Send as attachment |
filename | string | Custom download filename |
params | object | Input parameter values |
Unknown fields are allowed, so input-specific values can be included alongside the fields above.
Example Request:
{
"output": "pdf",
"download": true,
"filename": "quarterly-report.pdf",
"params": {
"report_title": "Q1 Sales Report",
"start_date": "2024-01-01",
"end_date": "2024-03-31",
"region": "north"
}
}
Response:
Returns the rendered document with the appropriate Content-Type and, when
download is set, a Content-Disposition header. The socket timeout is 60
minutes.
As with the GET route, the response is a binary document or a host-specific HTML/text string, so it is not captured as a docs fixture here.
POST /api/templates/{id}/_render-params
Resolve a map of typed input descriptors against a phase render context. This is used to preview resolved input values (for example, to populate the UI data grid) before a full render, and is the only JSON-returning endpoint on this page.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.write(pre.template)
Write access means hasFeature('templates') AND being a designer of the
Template. The template.lookup pre-block resolves the {id} natural id through
the read_access scope and answers 404 before the permission check or handler
runs when the Template is not visible to the caller.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
phaseContext | object | Yes | The render context available at the current phase (for example { inputs: { ... }, $data: [ ... ] }). Required because the full context may not exist yet when this is called from a processor. |
paramInputs | object | Yes | Map of output key to a typed input descriptor { entry, value } |
Each paramInputs entry resolves according to its entry type:
entry | Resolution |
|---|---|
literal | Pass value through unchanged |
template | Read phaseContext.inputs[value] |
expression | Evaluate value as a JavaScript expression in the render sandbox |
nunjucks | Render value as a Nunjucks template string |
data | Pass phaseContext.$data through |
record | Pluck the value field from each phaseContext.$data row |
Example Request:
{
"phaseContext": {
"inputs": {
"shipState": "NY"
},
"$data": [
{ "shipCity": "New York", "shipCountry": "US" },
{ "shipCity": "New York", "shipCountry": "US" }
]
},
"paramInputs": {
"shipCity": { "entry": "record", "value": "shipCity" },
"shipState": { "entry": "template", "value": "shipState" },
"shipDate": { "entry": "literal", "value": "2026-01-15" }
}
}
Response:
Responds 200 with a plain object { result: { <key>: <value> } }, one resolved
value per paramInputs key. The handler returns the object directly (no
.created()), so the status is 200, not 201, and there is no Location
header. A resolution failure is wrapped as 400.
{
"phaseContext": {
"inputs": {
"shipState": "NY"
},
"$data": [
{
"shipCity": "New York",
"shipCountry": "US"
},
{
"shipCity": "New York",
"shipCountry": "US"
}
]
},
"paramInputs": {
"shipCity": {
"entry": "record",
"value": "shipCity"
},
"shipState": {
"entry": "template",
"value": "shipState"
},
"shipDate": {
"entry": "literal",
"value": "2026-01-15"
}
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/admin%3Asales-order-report/_render-params"
}
},
"result": {
"shipCity": "New York",
"shipState": "NY",
"shipDate": "2026-01-15"
}
}
This route does not return a payloadId or expiresAt, and it does not write a
stored-payload row. It only resolves descriptors and returns their values.
Not found:
A Template id that does not exist (or is not visible to the caller) answers
404 from the template.lookup pre-block, before the permission check or
handler runs.
{
"phaseContext": {},
"paramInputs": {
"region": {
"entry": "literal",
"value": "north"
}
}
}
{
"statusCode": 404,
"error": "Not Found",
"message": "Not Found"
}