Invoice Template API
Read and publish the template your invoices are rendered with. The invoice template editor in the web interface is built on these three endpoints, and they are what you use to publish a template from your own code.
Every endpoint on this page accepts either an admin session token (Authorization: Bearer <JWT>)
or an admin-role API key in X-API-Key. See Authentication. Each one acts
on one environment, picked by the X-Environment header with a session token, or by the key's own
environment with an API key. Errors on this page come back as {"detail": ...}, not in the error
object other endpoints use.
A template is plain text. Everything in it is fixed except a few marked regions, such as the style, the header and the footer. The line-item tables and totals sit outside those regions, so no edit can break them.
Get the Template's Status
GET /api/v1/templates/invoice
Response:
{
"uploaded": true,
"managed": true,
"template_id": "12",
"name": "PortaAIM Default",
"state": { "v": 1, "style": { "...": "..." }, "logo": { "...": "..." }, "header_html": "..." },
"checksum": "9f2c1a0b..."
}
| Field | Description |
|---|---|
uploaded | true when this environment has an invoice template. |
managed | true when the template was published through this API, so its regions can be read back and changed. |
template_id | Id of the template. You do not need it to publish. |
name | Name of the template. |
state | The editor state the template was built from, recovered from the template itself. null when it is not managed. |
checksum | Checksum of the template in place. Send it back as expected_checksum when you publish. |
The status can be up to a minute old. After you publish, take the checksum for your next publish
from the publish response rather than reading the status again.
Get the Base Template
GET /api/v1/templates/invoice/base
Returns the base template as text/plain, the same for every tenant. Each editable region is
enclosed in a pair of comments, such as <!-- PAIM:HEADER:BEGIN --> and
<!-- PAIM:HEADER:END -->, and the markers are the list of regions. Build the template you publish
from this text by replacing only what sits between those pairs. The base can change between
releases, so fetch it again before each publish rather than storing an assembled template.
The state region carries the state you publish. Between <!-- PAIM:STATE:BEGIN --> and
<!-- PAIM:STATE:END -->, put one comment, <!-- PAIM:STATE <state> -->, where <state> is your
state object serialized as JSON and base64-encoded. Send the same object as state in the
request. If you edit only the HTML regions, an empty object {} is a valid state: every field
takes its default.
Publish the Template
PUT /api/v1/templates/invoice
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
tmpl | string | Yes | The complete template, at most 512 KiB. |
state | object | Yes | The editor state the template was built from. It must equal the state embedded in tmpl. The full shape is in the API reference at /docs. |
expected_checksum | string | No | The checksum you last read. Omit it to publish over whatever is in place. |
mirror_to_other_env | boolean | No | Also publish the same template to your other environment. Default false. |
Response:
{ "template_id": "12", "checksum": "4b7e90d1...", "mirrored": false }
Besides a 200, the request can answer:
422when validation refuses the template, and nothing is published.detailsays why. Everything outside the marked regions must match the base template byte for byte, the style region holds CSS only, the other regions refuse scripts and event handlers, template directives are limited to a fixed set of fields, and the logo must be a PNG or JPEG. A request body that does not match the schema also answers422, withdetailas a list of field errors.409when the template in place changed since you readexpected_checksum.detail.current_checksumcarries the current checksum, so read the status again and reapply your edit.400when you ask to mirror without a session for the other environment. An API key acts in one environment only, so mirroring needs a session token.502when the template was published to this environment but mirroring to the other one failed. Do not repeat the publish here; publish again from the other environment instead.
Mirroring carries no checksum check, so it publishes over whatever the other environment holds. The billing engine has no atomic swap either, so two publishes that land at the same moment end with the later one in place.