Appearance
Rendering
Rendering reports, jobs and their results, report links and the viewer's sessions.
POST /v1/render
Render a template.
Renders a published template (at commit, or the tip), a draft (draft: true, needs template.write) or an inline document. A preview (layout or png) with no datasets needs only template.read; anything else needs report.render.
Datasets come from the data plan. Supplied datasets are used as given (for a sourced dataset only under data.suppliedData: allow). A sourced dataset is fetched live from the version the published template pinned, through the environment's connection, when the caller has report.render and data.live, isn't an agent (unless agents.liveData) and isn't an MCP tool, and the copy is published or data is live. Everyone else gets the samples: the document's, else the source's, and so does every dataset left out (the engine's options.samples). data: sample always uses samples; data: live fails with 403 where live data isn't allowed. A render with live data uses no samples at all: a required dataset that is neither supplied nor fetched fails with 422 bind/missing-dataset (RFML §10.1). Fetches run in parallel under data.maxBytes (else the data limit) and fail closed: a cap is 413, a missing connection binding 409, a failed source 502 with a message that names no connection details, bad sourceParams 422 with bind/source-param diagnostics. X-Data-Origins says where each sourced or supplied dataset came from (name=live;source@version, name=sample, name=supplied).
Data over rfml.limits.dataBytes (or the engine's default) is refused with 413 before the engine is called. Engine limit errors (504 limit, 413 too-large) are passed on with their diagnostics; there is never partial output. PDF renders are stored (X-Report-Id).
html-paged and html-flow (RFML §17.5) are served under a strict Content-Security-Policy (no scripts, default-src 'none', inline styles and data: images only, sandbox, framed only by this host), with X-Content-Type-Options: nosniff and Cache-Control: no-store. Their fonts come from GET /v1/fonts/{sha}, which font-src allows ('self' and <origin>/v1/fonts/, the origin being publicURL or the request's). With options.embedFonts the faces are embedded as data: URLs instead (font-src data:), and the HTML is sent as an attachment that needs nothing else: the Export dialog's download.
report() links (RFML §17.6.4) in PDF, DOCX, XLSX and HTML from here are signed by the engine under a grant for the environment, valid for viewer.linkTTLSec: <origin>/l/v2.<grant>.<target>.<mac>, followed through GET /l/{token}. With the viewer off they are left out (output/link-unresolved).
The engine runs only the RFML extensions (§23) the environment allows (config extensions); others render as placeholders.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | ||
template | Document | ||
environment | string | ||
commit | string | ||
draft | boolean | ||
format | pdf, docx, xlsx, html-paged, html-flow, layout, png, svg | Default "pdf". | |
data | auto, sample, live | Where sourced datasets the request doesn't supply come from. Default "auto". | |
datasets | object | Data per dataset name. | |
params | object | ||
context | object | ||
context.now | string | RFC 3339 with an offset; defaults to the current time. | |
context.locale | string | ||
context.timezone | string | ||
options | object | ||
options.errorPolicy | fail, continue | ||
options.schemaMode | strict, lenient, off | ||
options.layoutDetail | pages, boxes, text | ||
options.pages | string | Page ranges, e.g. 1-3,7; png renders one page. | |
options.dpi | integer | ||
options.embedFonts | boolean | HTML only: embed the fonts and send the HTML as a download that works offline. | |
options.pdf | object | outputs.pdf settings (RFML §17.1) for this render, with the spec's names (standard, tagged, compress, embedFonts, metadata, attachments); each property given replaces the document's. | |
options.pdf.standard | none, PDF/A-2b, PDF/A-3b, PDF/UA-1, PDF/A-2a+UA-1 | ||
options.pdf.tagged | boolean |
Responses
| Status | Description | Body |
|---|---|---|
200 | PDF, DOCX, XLSX, HTML, PNG or SVG bytes (X-Content-Hash, X-Rfml-Pages, X-Rfml-Diagnostics; PDF, DOCX and XLSX as attachments), or the layout model as JSON | |
403 | Role or environment denied, or the data policy refused supplied or live data | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
409 | A source's connection has no binding for the environment | |
413 | The data is over the limit, or a source passed its cap | |
422 | The engine rejected the change or the document has errors: {error:"invalid", message, diagnostics}. A lock violation is an edit/locked diagnostic. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
501 | The engine has no writer for the format yet (not-implemented, with an output/not-implemented diagnostic) | |
502 | A data source failed | |
504 | The render stopped at a security/limit |
GET /v1/fonts/{sha}
A font rendered HTML uses.
The sfnt bytes (TTF or OTF) of a face by the lower-case hex SHA-256 the engine's HTML refers to it by (@font-face src, under options.fontBase /v1/fonts/): a bundled font by its manifest hash, or a template's font asset (a WOFF2 asset by the hash of its decoded bytes), from what renderd holds. Needs no sign-in: rendered HTML is sandboxed, so the browser fetches its fonts cross-origin and without cookies; the hash is the capability. Served with Cache-Control: public, max-age=31536000, immutable, an ETag, nosniff and Access-Control-Allow-Origin: *. Anyone who has a face's hash can read it, so a template's font assets need a licence that allows web use.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sha | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | The font | |
304 | Not modified (If-None-Match) | |
400 | Not a lower-case hex SHA-256 | |
404 | No font has that hash here | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/reports/{reportId}
Download a stored report.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
reportId | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | Bytes | |
404 | discard stores nothing | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/jobs
The caller's jobs, newest first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
status | query | queued, running, done, error, cancelled |
Responses
| Status | Description | Body |
|---|---|---|
200 | Jobs | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
200 body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
jobs | array of Job |
POST /v1/jobs
Render one template many times, in the background.
Queues a batch render of the published template: one render for each run, with that run's datasets and params, as the caller (looked up again, with the roles they have then, before each run). Jobs are kept in the template store, so they survive restarts, and carry on from the first run that hasn't rendered. A render that may pass later (the engine down, a timeout, a 429 or 5xx other than a render limit) is retried with a backoff, up to jobs.maxAttempts; any other failure ends the job as error. Finished jobs are kept for jobs.retentionHours, and a job that waits jobs.queueTimeoutHours to start ends as expired. With an Idempotency-Key, the caller's next submission with the same key, while the first job is kept, gets that job back (202, Idempotent-Replayed: true) instead of queueing another. With a callbackUrl, reportd POSTs to it once the job has finished (see callbacks).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | Makes the submission safe to repeat, such as after a timeout. 1 to 255 printable ASCII characters, chosen by the caller (a UUID, say), and the caller's own: other callers may use the same key. It must come with the same request each time. |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | yes | |
environment | string | ||
format | pdf, docx, xlsx, layout, png, svg, html-paged, html-flow | Default "pdf". | |
runs | array of object | yes | At most jobs.maxRuns (default 1000). |
runs[].datasets | object | ||
runs[].params | object | ||
callbackUrl | string (uri) | Called once the job has finished. An absolute http(s) URL, without credentials or a fragment, on an origin listed in the server's jobs.webhooks.endpoints. |
Responses
| Status | Description | Body |
|---|---|---|
202 | The job, queued; or, for a repeated Idempotency-Key, the job it first made, as it is now | Job |
400 | bad-request (no templateId, no runs, an unknown format, or an Idempotency-Key that isn't 1 to 255 printable ASCII characters) or callback-not-allowed (a callbackUrl that isn't on a configured endpoint) | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
422 | idempotency-key-reused (the caller used this Idempotency-Key for a different request) | |
429 | The render rate limit (TooManyRequests), or too-many-runs (more than jobs.maxRuns) or too-many-jobs (the caller already has jobs.maxQueued unfinished jobs) |
GET /v1/jobs/{jobId}
Job status.
Only its owner, or someone with user.manage, sees a job; anyone else gets 404.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | Job | Job |
404 | No such job, or not the caller's | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
DELETE /v1/jobs/{jobId}
Cancel a job.
A queued job is cancelled at once; a running one after the run it is rendering.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
202 | The job, cancelled or to be cancelled after its current run | Job |
404 | No such job, or not the caller's | |
409 | The job has already finished | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/view/sessions
Open a report in the interactive viewer.
Opens a view session (RFML §17.6) on a published report (or a draft, with template.write) for the caller, who needs report.view in the environment. The parameters are checked against the report's (422 invalid-params, with bind/param diagnostics), then the data is fetched once through the data plan: live where the caller has data.live (agents and MCP tools get samples), else the samples. datasets supplied by the caller (who then needs report.render, as for a render) are used as they are and never fetched again; for a sourced dataset only under data.suppliedData: allow (else 403 data-policy), and within viewer.maxBytes (else 413). Every later render of the session uses that data; changing the view state never queries a source again.
data.live is checked again on every call to the session. When the caller no longer has it, the live data the session holds is dropped and those datasets get their samples; a session opened with data: live is ended instead (403 data-policy, then 404).
The session's data is capped at viewer.maxBytes (413, security/limit), all sessions' at viewer.maxTotalBytes (503 viewer-full, Retry-After). Data past viewer.memoryBytes is spilled to disk, encrypted with a key that exists only in memory. A session not used for viewer.sessionTTLSec (default 30 minutes) is wiped, and so are a person's sessions when they log out. A session belongs to the principal that opened it; anyone else gets 404.
Served only when viewer.enabled; otherwise 404 viewer-disabled.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | yes | |
environment | string | ||
commit | string | A published commit; defaults to the tip. | |
draft | boolean | ||
params | object | ||
view | ViewState | ||
data | auto, sample, live | Default "auto". | |
datasets | object | Data per dataset name, supplied by the caller (needs report.render). | |
context | object | ||
context.now | string | RFC 3339 with an offset; defaults to when the session opens, and stays fixed for its life. | |
context.locale | string | ||
context.timezone | string |
Responses
| Status | Description | Body |
|---|---|---|
201 | The session. X-Data-Origins says where each dataset came from. | ViewSession |
400 | invalid-view-state or view-state-too-large | |
403 | No report.view in the environment, a draft without template.write, supplied data without report.render, or the data policy refused supplied or live data | |
404 | No such report, or viewer-disabled, or the template is in the trash (in-trash, see InTrash) | |
413 | The data (supplied or fetched) is over viewer.maxBytes or a source's cap | |
422 | invalid-params, or the data sources can't be called (bind/source-param) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
503 | viewer-full |
GET /v1/view/sessions/{sessionId}
Read a view session.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sessionId | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | The session | ViewSession |
404 | No such session for the caller, or it expired, or the template is in the trash (in-trash, see InTrash) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
PATCH /v1/view/sessions/{sessionId}
Change a session's view state or parameters (setParams).
view replaces the session's view state (checked as for opening). params sets parameter values (null goes back to the default): they are checked against the report's parameters, the engine's plan evaluates each dataset's sourceParams again, and only the live datasets whose sourceParams changed are fetched again. refetched lists them; it is empty when nothing was fetched.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sessionId | path | string | yes |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
params | object | ||
view | ViewState |
Responses
| Status | Description | Body |
|---|---|---|
200 | The session, with refetched | ViewSession |
400 | invalid-view-state or view-state-too-large | |
404 | No such session for the caller, or the template is in the trash (in-trash, see InTrash) | |
413 | The new data is over the cap | |
422 | invalid-params | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
DELETE /v1/view/sessions/{sessionId}
End a view session and wipe its data.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sessionId | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
204 | Ended | |
404 | No such session for the caller | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/view/sessions/{sessionId}/render
Render a view session.
Renders the session's data with its parameters and a view state: view from the query (the viewer's URL state, compact JSON, at most viewer.maxViewStateBytes, default 8 KB) for this render only, else the session's own. Nothing is fetched. Parameter values in view state are refused (400) unless viewer.viewStateParams allows them, and then work as setParams.
HTML (html-paged, html-flow) is served under the same strict Content-Security-Policy as /v1/render, with its fonts from /v1/fonts/{sha}. Its report() links are signed link tokens (/l/{token}) for the session's environment, valid for viewer.linkTTLSec (default 7 days). PDF, DOCX and XLSX exports are attachments whose report() links the engine signs under a grant, as in /v1/render; nothing is stored.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sessionId | path | string | yes | |
format | query | html-paged, html-flow, layout, svg, png, pdf, docx, xlsx | ||
view | query | string | View state (ViewState) as JSON. | |
pages | query | string | ||
dpi | query | integer | ||
layoutDetail | query | pages, boxes, text |
Responses
| Status | Description | Body |
|---|---|---|
200 | The rendered output, or the layout model | |
400 | An unsupported format, invalid-view-state or view-state-too-large | |
404 | No such session for the caller, or the template is in the trash (in-trash, see InTrash) | |
422 | The engine rejected the change or the document has errors: {error:"invalid", message, diagnostics}. A lock violation is an edit/locked diagnostic. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
504 | The render stopped at a security/limit |
POST /v1/links
Sign a link to a published report.
Signs a link token (HMAC with viewer.linkSecret) for a report, its environment, parameter values and an optional initial view state, valid for viewer.linkTTLSec. The caller needs report.view there; the parameters are checked as they will be when the link is followed. Viewer sessions sign the report() links in their HTML themselves; this is for hosts that build links of their own (mail, portals). Tokens are signed, not encrypted: parameter values can be read.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | yes | |
environment | string | ||
params | object | ||
view | ViewState |
Responses
| Status | Description | Body |
|---|---|---|
201 | The link | object |
400 | invalid-view-state | |
403 | No report.view in the environment | |
404 | The report isn't published there, or viewer-disabled, or the template is in the trash (in-trash, see InTrash) | |
422 | invalid-params | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
201 body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | |
url | string | yes | /l/{token}. |
expiresAt | string (date-time) | yes |
GET /l/{token}
Follow a report() link.
Checks the token's signature and expiry, that the reader has report.view in its environment and that its parameters fit the report, then opens a view session for the reader and redirects (303) to the viewer at viewer.path + session id (default /view/). Readers sign in first: the session cookie from /v1/login works here. A browser that isn't signed in (no Authorization header, Accept with text/html) is sent (303) to the studio's sign-in page, viewer.signInPath (default /login), with next=/l/{token}; the studio returns there after signing in. API callers get 401. A browser whose link can't be opened is sent (303) to the viewer app's page for why, viewer.path + ?error=invalid-link (400), forbidden (403), not-found (404), link-expired (410) or invalid-params (422); API callers get those statuses with JSON errors.
Tokens are v1 (from POST /v1/links and the viewer's HTML) or v2 (signed by the engine in exported documents, under a grant for one environment and expiry).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
token | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
303 | To the viewer, to the sign-in page, or (a browser) to the viewer's error page | |
400 | invalid-link (malformed, altered or signed with another key) | |
401 | Not signed in | |
403 | No report.view in the link's environment | |
404 | The linked report isn't published, or viewer-disabled, or the template is in the trash (in-trash, see InTrash) | |
410 | link-expired | |
422 | invalid-params | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |