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": [ … ] }.