Letterheads
Endpoints for managing Letterheads, which supply the header, footer, and watermark applied to templated PDF output. A Letterhead carries PDF layout options plus three embedded files (header, footer, watermark), and supports a per-user draft lifecycle so edits can be staged and committed without touching the live Letterhead.
There is no license-feature gate on any Letterhead route. Reads are open within
the tenant. Every write route is guarded by a Superuser permission check:
POST /api/letterheads requires permission.letterheads.create, while the
edit, delete, copy, and draft routes require permission.letterhead.edit,
permission.letterhead.delete, or permission.letterhead.copy, all of which
reduce to Superuser.
A Letterhead's naturalId is its slug or, when no slug exists, its id. The
model does not auto-generate a slug, so for API-created Letterheads the
naturalId is the UUID. Every \{id\} path segment resolves by UUID or slug.
GET /api/letterheads
List the tenant's Letterheads.
Authentication: Required (no special permission)
Response:
A HAL collection (rel inf:letterheads), with items embedded under
inf:letterhead. It filters to top-level Letterheads only (where editingId IS NULL), so per-user drafts never appear in the list. This is not a
{ total, limit, offset, results } envelope.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads"
}
},
"start": 0,
"count": 5,
"total": 5,
"_embedded": {
"inf:letterhead": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/draft-source-c"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/draft-source-c/_copy"
}
},
"naturalId": "draft-source-c",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Draft Source C",
"slug": "draft-source-c",
"description": null,
"options": null,
"createdById": "admin",
"editingId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/_copy"
}
},
"naturalId": "order-desk-letterhead",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"name": "Order Desk Letterhead",
"slug": "order-desk-letterhead",
"description": "Branded header and footer for Order Desk exports",
"options": null,
"createdById": "admin",
"editingId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/scratch-letterhead/_copy"
}
},
"naturalId": "scratch-letterhead",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000003",
"name": "Scratch Letterhead",
"slug": "scratch-letterhead",
"description": null,
"options": null,
"createdById": "admin",
"editingId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
"… 2 more items (5 total) — omitted from docs"
]
}
}
POST /api/letterheads
Create a Letterhead.
Authentication: permission.letterheads.create (Superuser)
Request Body:
The payload is stripUnknown, so unrecognized fields are dropped.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
description | string | No | Description |
header | string | No | Header HTML/Nunjucks; seeds the header.html file |
footer | string | No | Footer HTML/Nunjucks; seeds the footer.html file |
watermark | string | No | Watermark HTML/Nunjucks; seeds the watermark.html file |
options | object | No | PDF layout options (page size, margins, fonts, watermark opacity, etc.) |
The handler creates the Letterhead (createdById is the caller) and always
creates three embedded File rows (roles WATERMARK, HEADER, FOOTER, each
named <role>.html, text/html). When the matching header, footer, or
watermark string is supplied, that file is seeded with it.
Example Request:
{
"name": "Quarterly Report Letterhead",
"description": "Letterhead for quarterly PDF exports",
"header": "<div class=\"brand\">{{ tenant.name }}</div>",
"footer": "<div class=\"page\">Page {{ page }} of {{ pages }}</div>",
"watermark": "<div class=\"wm\">DRAFT</div>",
"options": {
"pageOrientation": "Automatic",
"pageSize": "letter",
"pageMargins": 10,
"fontName": "Roboto",
"fontSize": 11,
"headerHeight": 13,
"footerHeight": 5,
"watermarkOpacity": 25,
"watermarkAngle": 0
}
}
Response:
Responds 201 Created with the Letterhead and a Location header (built from
the naturalId). On any handler error it answers 400.
{
"name": "Quarterly Report Letterhead",
"description": "Letterhead for quarterly PDF exports",
"header": "<div class=\"brand\">{{ tenant.name }}</div>",
"footer": "<div class=\"page\">Page {{ page }} of {{ pages }}</div>",
"watermark": "<div class=\"wm\">DRAFT</div>",
"options": {
"pageOrientation": "Automatic",
"pageSize": "letter",
"pageMargins": 10,
"fontName": "Roboto",
"fontSize": 11,
"headerHeight": 13,
"footerHeight": 5,
"watermarkOpacity": 25,
"watermarkAngle": 0
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/quarterly-report-letterhead/_copy"
}
},
"naturalId": "quarterly-report-letterhead",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "Quarterly Report Letterhead",
"description": "Letterhead for quarterly PDF exports",
"options": {
"fontName": "Roboto",
"fontSize": 11,
"pageSize": "letter",
"pageMargins": 10,
"footerHeight": 5,
"headerHeight": 13,
"watermarkAngle": 0,
"pageOrientation": "Automatic",
"watermarkOpacity": 25
},
"createdById": "admin",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"slug": "quarterly-report-letterhead",
"editingId": null
}
GET /api/letterhead-templates
List the Letterhead builder presets.
Authentication: Required (no special permission)
Response:
This is not a CRUD list. It returns the presets discovered from the
letterhead-builder driver manager (rel inf:letterhead-templates). Two
built-in presets ship: a blank one and a "data export with watermark" one, each
carrying starter header/footer (and watermark) Nunjucks plus default PDF
options. Each item is shaped as { detective, id: "<detective>:<n>", value: { name, letterhead } }.
The detective value (and therefore each item id) is a random UUID generated
per server boot, so it changes across restarts. Treat it as opaque rather than a
stable identifier.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterhead-templates"
}
},
"items": [
{
"detective": "00000000-0000-4000-8000-000000000001",
"id": "00000000-0000-4000-8000-000000000001:0",
"value": {
"name": "Blank Letterhead",
"letterhead": {
"header": "<table style=\"width: 100%;\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td style=\"width: 75%;font-size: 11px\"></td>\n\t\t\t<td style=\"width: 25%;text-align:right;font-size: 11px\"></td>\n\t\t</tr>\n\t</tbody>\n</table>",
"footer": "<table style=\"width: 100%;\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td style=\"width: 50%;font-size:10px\"></td>\n\t\t\t<td style=\"width: 50%; font-size:10px; text-align: right;\"></td>\n\t\t</tr>\n\t</tbody>\n</table>",
"options": {
"pageOrientation": "Automatic",
"pageSize": "letter",
"pageMargins": 10,
"fontName": "Roboto, Helvetica, Arial, sans-serif",
"fontSize": 11,
"headerHeight": 13,
"footerHeight": 5,
"watermarkOpacity": 20,
"watermarkAngle": 0,
"watermarkScale": 1
}
}
}
},
{
"detective": "00000000-0000-4000-8000-000000000002",
"id": "00000000-0000-4000-8000-000000000002:0",
"value": {
"name": "Dataset export example",
"letterhead": {
"header": "<table style=\"width: 100%;\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td style=\"width: 75%;\">\n\t\t\t <span style=\"font-size: 14px;\"><strong>{{export.name}}</strong></span>\n\t\t\t <span style=\"font-size: 11px; color: rgb(85, 85, 85);\">\n {%if chipLabels %}\n <br>Filtered by: {{ chipLabels | join(', ')}}\n {% endif %}\n {%if groupLabels %}\n <br>Grouped by: {{ groupLabels | join(', ')}}\n {% endif %}\n </span>\n\t\t\t</td>\n\t\t\t<td style=\"width: 25%;text-align:right\">\n\t\t\t <img width=\"150\" height=\"75\" src=\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIxLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMzQgNDUuMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMjM0IDQ1LjM7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojNTQ2RjdBO30KCS5zdDF7ZmlsbDojMDI5N0E3O30KCS5zdDJ7ZmlsbDojODRDNjYxO30KCS5zdDN7ZmlsbDojRjA1MjIzO30KPC9zdHlsZT4KPGc+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNTMsNS4zYzAuOCwwLDEuNCwwLjMsMiwwLjhjMC41LDAuNiwwLjgsMS4zLDAuOCwyLjFjMCwwLjgtMC4zLDEuNS0wLjgsMi4xYy0wLjUsMC42LTEuMiwwLjktMiwwLjkKCQljLTAuNywwLTEuNC0wLjMtMS45LTAuOWMtMC41LTAuNi0wLjgtMS4zLTAuOC0yLjFjMC0wLjgsMC4zLTEuNCwwLjgtMkM1MS43LDUuNiw1Mi4zLDUuMyw1Myw1LjN6IE01MC43LDE1LjloNC43VjQxaC00LjdWMTUuOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik02Ni4zLDE1Ljl2My4yYzIuMS0yLjUsNC41LTMuNyw3LjEtMy43YzEuNSwwLDIuOCwwLjQsNC4xLDEuMmMxLjMsMC44LDIuMiwxLjksMi45LDMuM2MwLjcsMS40LDEsMy43LDEsNi43CgkJVjQxaC00LjdWMjYuN2MwLTIuNi0wLjQtNC40LTEuMS01LjVjLTAuNy0xLjEtMi0xLjctMy43LTEuN2MtMi4yLDAtNC4xLDEuMi01LjYsMy41djE4aC00LjhWMTUuOUg2Ni4zeiIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTg3LjksMTUuOXYtMS41YzAtNCwwLjgtNy4xLDIuNC05LjFjMS42LTIsMy42LTMsNS44LTNjMSwwLDIuMSwwLjIsMy41LDAuNXY0LjljLTAuOC0wLjMtMS41LTAuNC0yLjItMC40CgkJYy0xLjgsMC0zLjEsMC41LTMuOCwxLjZjLTAuNywxLTEuMSwyLjktMS4xLDUuNnYxLjVoNC4ydjQuNWgtNC4yVjQxaC00LjdWMjAuNGgtMy40di00LjVIODcuOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMTAuNywxNS44YzMuNiwwLDYuNiwxLjIsOC45LDMuN2MyLjQsMi41LDMuNiw1LjYsMy42LDkuM2MwLDMuNi0xLjIsNi42LTMuNiw5Yy0yLjQsMi40LTUuNCwzLjYtOS4xLDMuNgoJCWMtMy41LDAtNi41LTEuMi04LjktMy43Yy0yLjQtMi40LTMuNi01LjUtMy42LTkuMWMwLTMuNywxLjItNi44LDMuNi05LjJDMTA0LjEsMTcuMSwxMDcuMSwxNS44LDExMC43LDE1Ljh6IE0xMTAuNSwyMC4zCgkJYy0yLjIsMC00LjEsMC44LTUuNSwyLjRjLTEuNCwxLjYtMi4yLDMuNi0yLjIsNmMwLDIuNCwwLjcsNC40LDIuMiw1LjljMS41LDEuNSwzLjQsMi4zLDUuNywyLjNjMi4zLDAsNC4yLTAuOCw1LjYtMi4zCgkJYzEuNS0xLjUsMi4yLTMuNSwyLjItNS45YzAtMi40LTAuOC00LjQtMi4zLTUuOUMxMTQuNywyMS4xLDExMi44LDIwLjMsMTEwLjUsMjAuM3oiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMzIuOSwxNS45djUuOGwwLjMtMC40YzIuMy0zLjksNC41LTUuOCw2LjgtNS44YzEuOCwwLDMuNiwwLjksNS41LDIuOGwtMi41LDQuNGMtMS42LTEuNi0zLjEtMi41LTQuNS0yLjUKCQljLTEuNSwwLTIuOCwwLjgtMy45LDIuM2MtMS4xLDEuNS0xLjcsMy4zLTEuNyw1LjRWNDFoLTQuN1YxNS45SDEzMi45eiIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTE2Ny41LDIyLjhWNDFoLTQuN1YyN2MwLTIuOC0wLjQtNC43LTEuMS01LjhjLTAuNy0xLjEtMS45LTEuNy0zLjctMS43Yy0xLDAtMS45LDAuMi0yLjcsMC43CgkJYy0wLjgsMC41LTEuNywxLjMtMi44LDIuNVY0MUgxNDhWMTUuOWg0Ljd2My4zYzIuNC0yLjUsNC43LTMuOCw3LTMuOGMzLDAsNS4zLDEuNSw2LjksNC41YzIuNS0zLjEsNS4xLTQuNiw3LjgtNC42CgkJYzIuMywwLDQuMiwwLjksNS43LDIuN2MxLjUsMS44LDIuMiw0LjUsMi4yLDguMlY0MWgtNC43VjI2LjFjMC0yLjEtMC40LTMuNy0xLjItNC44Yy0wLjgtMS4xLTItMS43LTMuNS0xLjcKCQlDMTcxLDE5LjcsMTY5LjIsMjAuNywxNjcuNSwyMi44eiIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTIwOC4yLDI4LjloLTE2LjZjMC4xLDIuNCwwLjksNC4zLDIuMyw1LjdjMS40LDEuNCwzLjIsMi4xLDUuNCwyLjFjMy4xLDAsNS45LTEsOC41LTMuMXY0LjkKCQljLTEuNCwxLTIuOSwxLjgtNC4zLDIuMmMtMS40LDAuNC0zLjEsMC43LTUsMC43Yy0yLjYsMC00LjctMC42LTYuMy0xLjdjLTEuNi0xLjEtMi45LTIuNy0zLjktNC42Yy0xLTEuOS0xLjQtNC4yLTEuNC02LjcKCQljMC0zLjgsMS02LjksMy4xLTkuM2MyLTIuNCw0LjctMy42LDcuOS0zLjZjMy4xLDAsNS42LDEuMiw3LjUsMy41YzEuOSwyLjMsMi44LDUuNSwyLjgsOS40VjI4Ljl6IE0xOTEuNywyNS45aDExLjkKCQljLTAuMS0yLTAuNy0zLjUtMS43LTQuNmMtMS0xLjEtMi4zLTEuNi00LTEuNmMtMS43LDAtMywwLjUtNC4xLDEuNkMxOTIuOCwyMi40LDE5Mi4xLDIzLjksMTkxLjcsMjUuOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMTcuNywxNS45djUuOGwwLjMtMC40YzIuMy0zLjksNC41LTUuOCw2LjgtNS44YzEuOCwwLDMuNiwwLjksNS41LDIuOGwtMi41LDQuNGMtMS42LTEuNi0zLjEtMi41LTQuNS0yLjUKCQljLTEuNSwwLTIuOCwwLjgtMy45LDIuM2MtMS4xLDEuNS0xLjcsMy4zLTEuNyw1LjRWNDFoLTQuN1YxNS45SDIxNy43eiIvPgo8L2c+CjxnPgoJPHBhdGggY2xhc3M9InN0MSIgZD0iTTE0LjQsMTkuNmMxLjctMi4xLDQuMy0zLjQsNy4yLTMuNGM0LjksMCw4LjksMy43LDkuMyw4LjVoOS41Yy0wLjUtMTAtOC43LTE4LTE4LjgtMTgKCQljLTYuNCwwLTEyLjEsMy4yLTE1LjUsOC4xTDE0LjQsMTkuNnoiLz4KCTxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0yMy44LDM0LjdjLTAuNywwLjItMS40LDAuMy0yLjEsMC4zYy01LjIsMC05LjQtNC4yLTkuNC05LjRjMC0xLjIsMC4yLTIuNCwwLjctMy40bC04LjMtNC44CgkJYy0xLjIsMi41LTEuOSw1LjMtMS45LDguMmMwLDEwLjQsOC41LDE4LjksMTguOSwxOC45YzIuNSwwLDQuOC0wLjUsNy0xLjRMMjMuOCwzNC43eiIvPgoJPHBhdGggY2xhc3M9InN0MyIgZD0iTTMwLjgsMjcuN2MtMC42LDIuNS0yLjEsNC42LTQuMiw1LjlsNC43LDguMmM1LTMsOC40LTguMSw5LjEtMTQuMUgzMC44eiIvPgo8L2c+Cjwvc3ZnPgo=\">\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>",
"footer": "<table style=\"width: 100%;\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td style=\"width: 50%;\">\n\t\t\t <span style=\"font-size: 11px;\">{{export.dataUpdatedAt | date('LLL')}} {{user.displayName}} </span>\n </td>\n\t\t\t<td style=\"width: 50%; text-align: right;\">\n\t\t\t <span style=\"font-size: 11px;\">{{pageOfTotal}}</span>\n </td>\n\t\t</tr>\n\t</tbody>\n</table>",
"watermark": "<p><img src=\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIxLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzNy42IDM3LjgiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDM3LjYgMzcuODsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMyMTk2RjM7fQoJLnN0MXtmaWxsOiM4NEM2NjE7fQoJLnN0MntmaWxsOiNGRjU3MjI7fQo8L3N0eWxlPgo8Zz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMS42LDEyLjljMS43LTIuMSw0LjMtMy40LDcuMi0zLjRjNC45LDAsOC45LDMuNyw5LjMsOC41aDkuNUMzNy4xLDgsMjguOSwwLDE4LjgsMGMtNi40LDAtMTIsMy4yLTE1LjQsOC4xCgkJTDExLjYsMTIuOXoiLz4KCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0yMSwyOGMtMC43LDAuMi0xLjQsMC4zLTIuMSwwLjNjLTUuMiwwLTkuNC00LjItOS40LTkuNGMwLTEuMiwwLjItMi40LDAuNy0zLjRsLTguMy00LjhDMC43LDEzLjIsMCwxNiwwLDE4LjkKCQljMCwxMC40LDguNSwxOC45LDE4LjksMTguOWMyLjUsMCw0LjgtMC41LDctMS40TDIxLDI4eiIvPgoJPHBhdGggY2xhc3M9InN0MiIgZD0iTTI4LDIwLjljLTAuNiwyLjUtMi4xLDQuNi00LjIsNS45bDQuNyw4LjJjNS0zLDguNC04LjEsOS4xLTE0LjFDMzcuNiwyMC45LDI4LDIwLjksMjgsMjAuOXoiLz4KPC9nPgo8L3N2Zz4K\" style=\"width: 300px; margin: auto;\"></p>",
"options": {
"pageOrientation": "Automatic",
"pageSize": "letter",
"pageMargins": 10,
"fontName": "Roboto, Helvetica, Arial, sans-serif",
"fontSize": 11,
"headerHeight": 13,
"footerHeight": 5,
"watermarkOpacity": 20,
"watermarkAngle": 0,
"watermarkScale": 1
}
}
}
}
],
"start": 0,
"count": 2,
"total": 2
}
GET /api/letterheads/{id}
Retrieve a single Letterhead.
Authentication: Required (no special permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
Response:
Returns the single Letterhead (HAL rel inf:letterhead). Links include
inf:sample, inf:header, inf:footer, and inf:watermark. A non-draft
Letterhead also exposes inf:draft, inf:edit, and inf:copy; a draft instead
exposes inf:commit-draft.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/_copy"
}
},
"naturalId": "order-desk-letterhead",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Order Desk Letterhead",
"slug": "order-desk-letterhead",
"description": "Branded header and footer for Order Desk exports",
"options": null,
"createdById": "admin",
"editingId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
Lookups are tenant-scoped (RLS) but not owner-scoped: any Superuser in the tenant can read or edit any Letterhead.
PUT /api/letterheads/{id}
Update a Letterhead's properties.
Authentication: permission.letterhead.edit (Superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | Display name |
description | string | Description |
options | object | PDF layout options |
header, footer, and watermark cannot be changed through this route. Write
file bodies through PUT /api/letterheads/{id}/files/{role} instead.
Example Request:
{
"name": "Order Desk Letterhead (Updated)",
"description": "Refreshed branding for Order Desk exports",
"options": { "pageSize": "legal", "pageMargins": 12 }
}
Response:
Responds 200 with the updated Letterhead.
{
"name": "Order Desk Letterhead (Updated)",
"description": "Refreshed branding for Order Desk exports",
"options": {
"pageSize": "legal",
"pageMargins": 12
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/order-desk-letterhead/_copy"
}
},
"naturalId": "order-desk-letterhead",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Order Desk Letterhead (Updated)",
"slug": "order-desk-letterhead",
"description": "Refreshed branding for Order Desk exports",
"options": {
"pageSize": "legal",
"pageMargins": 12
},
"createdById": "admin",
"editingId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
POST /api/letterheads/{id}/_copy
Deep-copy a Letterhead and all of its files.
Authentication: permission.letterhead.copy and permission.letterheads.create (both Superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
Request Body:
The payload is stripUnknown.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Name for the copy |
description | string | No | Description for the copy |
progress | string | No | UI/streaming progress hint; stripped before the copy |
The copy sets createdById to the caller.
Example Request:
{ "name": "Copy of Order Desk Letterhead" }
Response:
Responds 201 Created with the new Letterhead and a Location header. On any
error it answers 422.
{
"name": "Copy of Order Desk Letterhead"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/copy-of-order-desk-letterhead/_copy"
}
},
"naturalId": "copy-of-order-desk-letterhead",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"name": "Copy of Order Desk Letterhead",
"slug": "copy-of-order-desk-letterhead",
"description": "Refreshed branding for Order Desk exports",
"options": {
"pageSize": "legal",
"pageMargins": 12
},
"createdById": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"editingId": null
}
GET /api/letterheads/{id}/files/{role}
Get the contents of one of a Letterhead's files.
Authentication: Required (no special permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
role | string | One of watermark, header, footer (case-insensitive) |
Response:
Returns the file contents as the raw body (a text/html string,
download:false), not a HAL or JSON envelope. Any role other than
watermark, header, or footer answers 400 at validation.
"<div class=\"brand\">Order Desk</div>"
PUT /api/letterheads/{id}/files/{role}
Replace the contents of one of a Letterhead's files.
Authentication: permission.letterhead.edit (Superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
role | string | One of watermark, header, footer (case-insensitive) |
Request Body:
A raw, unparsed stream of the file bytes (output: 'stream', parse: false).
The new HTML/Nunjucks body is sent as the request body, not as a JSON field.
Response:
Responds with an empty body.
This route streams opaque file bytes in and returns an empty body, so there is no meaningful request/response pair to capture as an example.
GET /api/letterheads/{id}/sample.pdf
Render a sample PDF preview of the Letterhead.
Authentication: Required (no special permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
Response:
Renders a sample PDF (via Puppeteer / wkhtmltopdf) and returns
application/pdf bytes.
The response is binary PDF output produced by a heavy render step, so it is not captured as an example.
DELETE /api/letterheads/{id}
Delete a Letterhead.
Authentication: permission.letterhead.delete (Superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Letterhead UUID or slug |
Response:
Responds 200 with an empty body (Informer's db.remove convention), not
204.
Deleting a Letterhead also removes its embedded File rows (header, footer, and watermark).
POST /api/letterheads/{id}/_edit
Start or resume a per-user Letterhead draft.
Authentication: permission.letterhead.edit (Superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original Letterhead UUID or slug |
Response:
Returns the caller's existing draft for this Letterhead if one exists, otherwise
creates one. Creating a draft copies the options and all files, sets
editingId to the original id, names it <name> Draft, and sets createdById
to the caller. A draft is itself a Letterhead row with editingId set. Responds
200 with HAL rel inf:letterhead-edit.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001/_edit"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/files/watermark"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001/draft/_commit"
}
},
"naturalId": "draft-source-a-draft",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"name": "Draft Source A draft",
"slug": "draft-source-a-draft",
"description": null,
"options": null,
"createdById": "admin",
"editingId": "00000000-0000-4000-8000-000000000001",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
A draft is keyed by createdById, so each Superuser gets their own draft of a
given Letterhead. This route is idempotent: resuming returns the existing draft
rather than creating a second one.
GET /api/letterheads/{id}/draft
Get the caller's current draft for a Letterhead.
Authentication: Required (no special permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original Letterhead UUID or slug |
Response:
Returns the caller's draft for this Letterhead (HAL rel
inf:letterhead-draft), with an inf:original link back to the source. The
\{id\} is the original Letterhead id; the draft is resolved per caller.
This route answers 404 when the caller has no draft. Create one with
POST /api/letterheads/{id}/_edit first.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000002"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/draft-source-a-draft/files/watermark"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:original": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001"
}
},
"naturalId": "draft-source-a-draft",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"name": "Draft Source A draft",
"slug": "draft-source-a-draft",
"description": null,
"options": null,
"createdById": "admin",
"editingId": "00000000-0000-4000-8000-000000000001",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
POST /api/letterheads/{id}/draft/_commit
Commit the caller's draft back onto the original Letterhead.
Authentication: permission.letterhead.edit (Superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original Letterhead UUID or slug |
Response:
Applies the caller's draft to the original (copies options, replaces the
original's files with the draft's, and destroys the draft), then returns the
committed original Letterhead. Responds 200.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/letterheads/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:sample": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/sample.pdf"
},
"inf:header": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/files/header"
},
"inf:footer": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/files/footer"
},
"inf:watermark": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/files/watermark"
},
"inf:draft": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/_edit"
},
"inf:copy": {
"href": "https://informer.example.com/api/letterheads/draft-source-b/_copy"
}
},
"naturalId": "draft-source-b",
"permissions": {
"copy": true,
"edit": true,
"delete": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Draft Source B",
"slug": "draft-source-b",
"description": null,
"options": null,
"createdById": "admin",
"editingId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
DELETE /api/letterheads/{id}/draft
Discard the caller's draft for a Letterhead.
Authentication: permission.letterhead.delete (Superuser, checked on the draft row)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original Letterhead UUID or slug |
Response:
Resolves the caller's draft, checks permission.letterhead.delete on the draft
row, and discards it. Responds 200 with an empty body (db.remove). The
original Letterhead is untouched.