Rendering documents
Rendering is the core job of this service: you give it a template (an .xlsx
or .docx file) plus a JSON blob of data, and it gives you back a finished
document in the format you ask for — a PDF, a self‑contained HTML page, image
pages, or the filled‑in source file itself.
This page explains how the render pipeline behaves today, in the web UI, the CLI, and the HTTP API. For the machine‑facing request/response reference, see the API reference.
The mental model
Every render is the same three‑part equation:
template (.xlsx / .docx) + data (JSON) → output (pdf / html / jpg / webp / original)
- Template — an Office document with
{d.*}placeholders where dynamic values should go. The service auto‑detects whether it is a spreadsheet (XLSX) or a Word document (DOCX) by looking inside the file; you never have to tell it. - Data — a JSON object. Each
{d.field}placeholder is replaced by the matching value. Data is optional: render a template with no data and you get the template's literal text. - Output format — what you want back. One of
original,pdf,html,jpg, orwebp(see Output formats).
The same template can be rendered to any output format with the same data — the format is just a parameter on the request.
Output formats
The format parameter selects what comes back. It accepts five values:
format | What you get | MIME type |
|---|---|---|
original | The source file with data substituted — a filled‑in .xlsx or .docx. The shape of the document is untouched; only {d.*} placeholders change. | …spreadsheetml.sheet / …wordprocessingml.document |
pdf (default) | A print‑ready PDF. Can be digitally signed in the same request (see Signing). | application/pdf |
html | A self‑contained HTML page — one inline SVG per page, no external assets, opens straight in a browser. | text/html; charset=utf-8 |
jpg | Rasterised JPEG image pages. | image/jpeg (single page) / application/zip (multi‑page) |
webp | Rasterised WebP image pages (lossless VP8L). | image/webp (single page) / application/zip (multi‑page) |
format=csv is rejected with HTTP 400. It was removed deliberately —
CSV cannot represent the styled, paginated layout the renderer produces. Use
original if you need the raw spreadsheet data.
Single page vs. multi‑page images
For jpg and webp, the response shape depends on the page count:
- One page → the bare image is returned directly (
image/jpeg/image/webp). - More than one page → the pages are packed into a ZIP archive
(
application/zip) with entries namedpages/page-001.jpg,pages/page-002.jpg, … (zero‑padded to the total page count).
This keeps simple one‑page callers trivial while still handling long documents.
Template data substitution
Placeholders use Carbone‑style {d.*} syntax. Pass your data as a JSON object
(the service automatically wraps a plain object so {d.company} resolves
against the top level).
{ "company": "Acme Corp", "invoice_no": "INV-001", "paid": true }| Placeholder | Resolves to |
|---|---|
{d.company} | Acme Corp |
{d.invoice_no} | INV-001 |
{d.customer.name} | nested field lookup with dotted paths |
Conditionals
Placeholders can carry inline conditional operators:
| Syntax | Behaviour |
|---|---|
{d.field:ifEQ('x'):show('yes'):elseShow('no')} | Show yes when field == x, otherwise no |
{d.field:ifNE('x'):show('yes')} | Show yes when field != x |
Loops and images (DOCX)
DOCX templates additionally support repetition blocks — {#items}…{/items}
expands once per element of an array — and image injection, where a
{d.*} value that points at an image URL/path is embedded into the document's
media folder. Word frequently splits a single placeholder across multiple
internal runs as you edit it; the converter re‑joins those runs before
substituting, so placeholders survive ordinary editing.
What happens under the hood
You don't need to know this to use the service, but it explains the formats' characteristics.
XLSX pipeline — the spreadsheet is parsed, {d.*} placeholders are
replaced in cells and drawings, each sheet is laid out as an intermediate
styled HTML table (colours, borders, merged cells, absolutely‑positioned
images and shapes), paginated to the chosen page size, and handed to
LibreOffice to produce the PDF.
DOCX pipeline — the OOXML is unzipped, runs are merged, loops and
conditionals are expanded, placeholders and images are substituted, the package
is rezipped, and LibreOffice produces the PDF (or serves the filled DOCX
directly for format=original).
From PDF to the other formats — html, jpg, and webp are all produced
from the PDF:
html→ the PDF is converted to one SVG per page (viapdftocairo, rendered in parallel) and assembled into a single self‑contained HTML wrapper.jpg/webp→ the PDF is rasterised page‑by‑page (viapdftoppm); WebP pages are re‑encoded in‑process by a pure‑Go encoder.
Image‑heavy documents can produce large html files (tens of MB) because
every page is an inline SVG. That is fine for a one‑shot export, but not
intended for interactive preview.
Image quality defaults
Rasterised output (jpg / webp) uses these built‑in defaults:
| Setting | Default | Range |
|---|---|---|
| DPI | 150 | clamped to 72–300 |
| JPEG quality | 85 | 1–100 |
| WebP quality | 80 | 1–100 (controls compression effort; output is lossless) |
| Max pages | 200 | hard cap on pages rasterised |
Signing PDFs
When format=pdf, the render can produce a digitally signed (PAdES) PDF in
the same call — signing is the final step of the pipeline. You reference a
signing certificate that was previously uploaded to your organization (see the
Signing Certs API), and may supply metadata such as reason, location,
contact, a timestamp‑authority (TSA) URL, and an optional visible signature
rectangle. Signing applies only to pdf; it is ignored for every other format.
The CLI can also sign a PDF you already have, independently of rendering — see
calco sign below.
Ways to render
Web UI
Open a template under Plantillas, choose a version, supply data, and pick the output format. The dashboard renders on demand and previews the result.
CLI
The single calco binary renders from the command line:
# Convert a template to PDF with data
go run ./cmd/calco convert \
-in examples/proalt_template.xlsx \
-data examples/payload.json \
-format pdf \
-out out.pdf
# Render and sign in one step
go run ./cmd/calco convert -in template.xlsx -data payload.json \
-format pdf -sign -cert mycert.p12 -password '…' -out signed.pdf
# Sign an existing PDF
go run ./cmd/calco sign -in in.pdf -out signed.pdf -cert mycert.p12 -password '…'
# Verify signatures in a PDF (JSON report to stdout)
go run ./cmd/calco verify -in signed.pdf-format accepts the same values as the API (original, pdf, html, jpg,
webp).
HTTP API
The relevant render endpoints (full schemas in the API reference):
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /convert | session / api‑key | One‑shot stateless convert — upload a file, get a rendered document, nothing stored. |
| POST | /api/templates/{id}/versions/{version}/render | session | Render a stored template version. |
| GET | /api/templates/{id}/versions/{version}/download | session | Same render, parameters as query string (easy to link). |
| POST / GET | /api/render/{hash} | api‑key | Hash‑first machine endpoint — identify the template version by its file SHA‑256 and render it. |
| GET | /api/render/{hash}/download | api‑key | Hash‑first render via query string. |
| POST | /api/batch-render | session | Mail‑merge — render one template against many records, get a ZIP back. |
Common request fields (multipart form or query): format, data_json,
sheet (XLSX), title, plus the sign_* fields when signing. The response
carries a Content-Disposition filename derived from the title or upload name.
Hash‑first workflow
For machine‑to‑machine integration, address templates by the SHA‑256 hash of the file rather than by an ID you have to store:
- Upload a template once → receive its hash.
- Reference it forever by that hash.
- Use
HEAD /api/render/{hash}to cheaply check whether a version is already registered before re‑uploading.
The hash is looked up within the API key's organization, so the same hash can map to different templates in different orgs.
Batch render
POST /api/batch-render renders the same template once per record and returns a
ZIP. Each item in records_json is shaped {hash, title?, data}. Records that
fail are collected into an errors.csv (columns index,hash,error) at the
archive root instead of aborting the whole job. Limits are configurable via
BATCH_* environment variables (max items, max distinct templates, max archive
size, per‑request timeout); query the current limits with
GET /api/batch-render/formats.
Render caching
Stored‑template renders are cached: an identical render (same template, version,
data, sheet, title, and format) is served from cache and the response carries
X-Render-Cache: hit. The cache driver is pluggable (none, memory,
local, s3).
Full API & OpenAPI spec
The complete machine‑facing API — every endpoint, parameter, and response schema, plus how to download the OpenAPI document and import it into Postman — lives on the dedicated API reference page.