PDF documents API
Manage layout templates, generate PDFs from records and list generated files.
The Templates app turns a record (an invoice, a job card, a quotation) into a PDF using a layout you design. This API manages those layouts, generates PDFs and lists what was generated. Generated files are saved to Drive.
If the Templates app is switched off in Settings › Apps, these routes answer 404 NOT_FOUND.
Endpoints
| Method | Path | Permission | What it does |
|---|---|---|---|
GET |
/orgs/{orgId}/pdf/templates |
pdf:read |
List templates |
POST |
/orgs/{orgId}/pdf/templates |
pdf:manage |
Create a template |
GET |
/orgs/{orgId}/pdf/templates/{tmplId} |
pdf:read |
Get a template |
PUT |
/orgs/{orgId}/pdf/templates/{tmplId} |
pdf:manage |
Replace a template |
DELETE |
/orgs/{orgId}/pdf/templates/{tmplId} |
pdf:manage |
Delete a template |
POST |
/orgs/{orgId}/pdf/templates/{tmplId}/render |
pdf:generate |
Generate and save a PDF |
POST |
/orgs/{orgId}/pdf/preview |
pdf:manage |
Render unsaved layout source and return the PDF |
GET |
/orgs/{orgId}/pdf/generations |
pdf:read |
List generated files |
The template object
{
"id": "0190fb00-…",
"name": "Invoice",
"description": "Customer invoice",
"object_type": "invoices",
"bucket_id": "0190f700-…",
"source": "<document page=\"A4\" margin=\"14mm\">…</document>",
"created_at": "2026-10-01T08:00:00Z",
"updated_at": "2026-10-01T08:00:00Z"
}
POST /orgs/{orgId}/pdf/templates
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | Yes | |
description |
string | No | |
object_type |
string | No | The record type the template is written for. It must exist. Leave empty for a template that takes only data |
bucket_id |
string | Yes | The Drive space generated files go into. It must be a team space, not a personal drive |
source |
string | Yes | The layout, up to 256 KB. See below |
GET /pdf/templates answers { "templates": [ … ] }; POST answers 201; DELETE answers 200 { "deleted": true }.
The layout is checked when you save it, over sample data, and again every time it renders. 400 VALIDATION_ERROR lists every problem found. PUT takes the same body.
The layout language
A layout is XML with Go-template placeholders. Values you print are escaped automatically, so a record value such as R&D <Ltd> always appears as text and can never change the layout.
<document page="A4" margin="14mm" font-size="10">
<class name="muted" color="#6b7280" font-size="9"/>
<header>
<row valign="bottom">
<col w="8"><h2>{{.org.name}}</h2></col>
<col w="4" align="right"><h1>{{.record.values.number}}</h1></col>
</row>
<hr/>
</header>
<footer><text class="muted" align="right">Page {page} of {pages}</text></footer>
<text>Issued {{date .record.values.issue_date}}</text>
<spacer h="10"/>
<table lines="rows">
<column w="3"/><column w="1"/>
<tr header="true"><td>Item</td><td>Amount</td></tr>
<tr><td>Consulting</td><td>{{money .record.values.amount}}</td></tr>
</table>
</document>
Building blocks: <document> (page size, margin, base font), <class> (named styles), <header> and <footer> (with {page} and {pages}), <text> and headings <h1> to <h3> (with inline <b>, <i>, <span>, <br/>), <row> with <col w="…"> on a 12-column grid (the widths of a row add up to 12, or all are left out), <box>, <table> with <column>, <tr header="true"> and <td>, <image> (inline data: images), <hr/>, <spacer> and <pagebreak/>. Tables continue across pages with the header row repeated.
Values available to the template:
| Placeholder | What it holds |
|---|---|
.org |
name, default_currency |
.now |
The time of generation |
.record |
id, type, created_at, and values (the record's fields, already limited to what the caller may read) |
.refs.<field_key> |
The record a reference field points at, one level deep, with its own values (up to 50). Wrap uses in {{if .refs.customer}}…{{end}} |
.data |
The extra values you pass when rendering |
Helper functions: money, date, datetime, upper, lower. Standard {{if}}, {{range}} and {{with}} work.
POST /orgs/{orgId}/pdf/templates/{tmplId}/render
Generate a PDF and save it. Permission: pdf:generate on the template's record type (or * for a template with no type), and write access to the template's Drive space. Values come from the record as the caller sees them: hidden fields are masked, and references the caller cannot read are skipped.
| Field | Type | Required | Notes |
|---|---|---|---|
record_id |
string | No | The record to print. The template must be bound to a record type. Without it the template renders only from data |
data |
object | No | Extra values, available as .data |
curl -s -X POST https://axisiq.co/api/v1/orgs/$ORG/pdf/templates/$TMPL/render \
-H "Authorization: Bearer $AXIS_KEY" -H "Content-Type: application/json" \
-d '{"record_id": "0190f0aa-…"}'
Response 201
{
"data": {
"id": "0190fb10-…",
"template_id": "0190fb00-…",
"record_id": "0190f0aa-…",
"object_type": "invoices",
"bucket_id": "0190f700-…",
"key": "generated/invoice/inv-1001-20261004-093000-0f3a9c1d.pdf",
"size": 31204,
"created_by": "0190f0a0-…",
"created_at": "2026-10-04T09:30:00Z"
}
}
The key is generated from the template name, the record and the time, under generated/. Download the file from Drive with GET /drive/buckets/{bucket_id}/download?key={key}.
Errors: 400 VALIDATION_ERROR (layout problem), 403 FORBIDDEN, 404 NOT_FOUND (template, record or space).
POST /orgs/{orgId}/pdf/preview
Body: source (required), object_type, record_id, data. Returns the PDF itself (Content-Type: application/pdf), not saved anywhere. Use it while designing a layout.
GET /orgs/{orgId}/pdf/generations
List generated files, newest first (the latest 200), optionally filtered by record_id or template_id. Returns { "generations": [ … ] }.
Related
- Drive
- Functions SDK:
axis.pdf.render - In the product: Templates