Skip to main content

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.

List letterheadsGET /api/letterheads
GET /api/letterheads is a HAL collection (rel inf:letterheads) of the tenant’s letterheads, items embedded under inf:letterhead. It filters to top-level letterheads only (where editingId IS NULL), so per-user drafts never appear. It is NOT a {total,limit,offset,results} envelope.
Response · 200
{
"_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"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/letterheads

Create a Letterhead.

Authentication: permission.letterheads.create (Superuser)

Request Body:

The payload is stripUnknown, so unrecognized fields are dropped.

FieldTypeRequiredDescription
namestringYesDisplay name
descriptionstringNoDescription
headerstringNoHeader HTML/Nunjucks; seeds the header.html file
footerstringNoFooter HTML/Nunjucks; seeds the footer.html file
watermarkstringNoWatermark HTML/Nunjucks; seeds the watermark.html file
optionsobjectNoPDF 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.

Create a letterheadPOST /api/letterheads
POST /api/letterheads requires permission.letterheads.create (superuser). Payload is stripUnknown: { name (required), description?, header?, footer?, watermark?, options? }. The handler creates the letterhead (createdById = the caller) and ALWAYS creates three embedded File rows (WATERMARK/HEADER/FOOTER, each `${role}.html`, text/html); a supplied header/footer/watermark string seeds the matching file. Responds 201 with a Location header. naturalId = slug || id, and the model does not auto-generate a slug, so naturalId is the uuid.
Request body
{
"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 · 201
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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 } }.

Per-boot detective ids

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.

List letterhead builder presetsGET /api/letterhead-templates
GET /api/letterhead-templates 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 (blank, data export with watermark), each with starter header/footer/(watermark) Nunjucks and default PDF options. Each item is { detective, id: `${detective}:<n>`, value: { name, letterhead } }; the detective id is a per-boot random uuid.
Response · 200
{
"_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')}} &nbsp; &nbsp; &nbsp; {{user.displayName}}&nbsp;</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
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/letterheads/{id}

Retrieve a single Letterhead.

Authentication: Required (no special permission)

Path Parameters:

ParameterTypeDescription
idstringLetterhead 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.

Get a letterheadGET /api/letterheads/00000000-0000-4000-8000-000000000001
GET /api/letterheads/{id} returns the single letterhead (HAL rel inf:letterhead). The id resolves by uuid OR slug. Links include inf:sample, inf:header, inf:footer, inf:watermark, and (for a non-draft) inf:draft, inf:edit, inf:copy.
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.
Tenant-scoped, not owner-scoped

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:

ParameterTypeDescription
idstringLetterhead UUID or slug

Request Body:

FieldTypeDescription
namestringDisplay name
descriptionstringDescription
optionsobjectPDF layout options
Files are not editable here

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.

Update a letterheadPUT /api/letterheads/00000000-0000-4000-8000-000000000001
PUT /api/letterheads/{id} requires permission.letterhead.edit (superuser). Payload is { name?, description?, options? }; header/footer/watermark are NOT editable here (use PUT /api/letterheads/{id}/files/{role}). Returns the updated letterhead.
Request body
{
"name": "Order Desk Letterhead (Updated)",
"description": "Refreshed branding for Order Desk exports",
"options": {
"pageSize": "legal",
"pageMargins": 12
}
}
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

ParameterTypeDescription
idstringLetterhead UUID or slug

Request Body:

The payload is stripUnknown.

FieldTypeRequiredDescription
namestringNoName for the copy
descriptionstringNoDescription for the copy
progressstringNoUI/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.

Copy a letterheadPOST /api/letterheads/00000000-0000-4000-8000-000000000001/_copy
POST /api/letterheads/{id}/_copy requires BOTH permission.letterhead.copy and permission.letterheads.create (both superuser). It deep-copies the letterhead plus all its files. Payload is { name?, description?, progress? } (stripUnknown); `progress` is stripped before the copy. Sets createdById = the caller. Responds 201 with a Location header.
Request body
{
"name": "Copy of Order Desk Letterhead"
}
Response · 201
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/letterheads/{id}/files/{role}

Get the contents of one of a Letterhead's files.

Authentication: Required (no special permission)

Path Parameters:

ParameterTypeDescription
idstringLetterhead UUID or slug
rolestringOne 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.

Get a letterhead fileGET /api/letterheads/00000000-0000-4000-8000-000000000001/files/header
GET /api/letterheads/{id}/files/{role} returns the file contents as the RAW body (text/html string, download:false), NOT a HAL/JSON envelope. `role` is one of watermark|header|footer (case-insensitive); any other value 400s at validation. Here the header file seeded at create time is returned.
Response · 200
"<div class=\"brand\">Order Desk</div>"
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/letterheads/{id}/files/{role}

Replace the contents of one of a Letterhead's files.

Authentication: permission.letterhead.edit (Superuser)

Path Parameters:

ParameterTypeDescription
idstringLetterhead UUID or slug
rolestringOne 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.

Example not shown

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:

ParameterTypeDescription
idstringLetterhead UUID or slug

Response:

Renders a sample PDF (via Puppeteer / wkhtmltopdf) and returns application/pdf bytes.

Example not shown

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:

ParameterTypeDescription
idstringLetterhead UUID or slug

Response:

Responds 200 with an empty body (Informer's db.remove convention), not 204.

Delete a letterheadDELETE /api/letterheads/00000000-0000-4000-8000-000000000001
DELETE /api/letterheads/{id} requires permission.letterhead.delete (superuser) and cascades to the embedded File rows. Returns 200 with an EMPTY body (Informer db.remove convention), NOT 204.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Cascade delete

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:

ParameterTypeDescription
idstringOriginal 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.

Start or resume a letterhead draftPOST /api/letterheads/00000000-0000-4000-8000-000000000001/_edit
POST /api/letterheads/{id}/_edit requires permission.letterhead.edit (superuser). It returns the caller’s existing draft for this letterhead if one exists, otherwise creates one (createDraft copies options + all files, sets editingId = the original id, name = "<name> Draft", createdById = the caller). 200, HAL rel inf:letterhead-edit. A draft is itself a Letterhead row with editingId set. (Idempotent: this capture resolves the draft pre-created in setup.)
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.
Drafts are per user

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:

ParameterTypeDescription
idstringOriginal 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.

No draft yet

This route answers 404 when the caller has no draft. Create one with POST /api/letterheads/{id}/_edit first.

Get the current letterhead draftGET /api/letterheads/00000000-0000-4000-8000-000000000001/draft
GET /api/letterheads/{id}/draft 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 via letterhead.draft(originalId, userId). 404 if the caller has no draft yet (create one via POST .../_edit first).
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/letterheads/{id}/draft/_commit

Commit the caller's draft back onto the original Letterhead.

Authentication: permission.letterhead.edit (Superuser)

Path Parameters:

ParameterTypeDescription
idstringOriginal 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.

Commit a letterhead draftPOST /api/letterheads/00000000-0000-4000-8000-000000000001/draft/_commit
POST /api/letterheads/{id}/draft/_commit requires permission.letterhead.edit (superuser). It applies the caller’s draft back onto the original (copies options, replaces the original’s files with the draft’s, destroys the draft) and returns the committed original letterhead. 200.
Response · 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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

ParameterTypeDescription
idstringOriginal 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.

Discard a letterhead draftDELETE /api/letterheads/00000000-0000-4000-8000-000000000001/draft
DELETE /api/letterheads/{id}/draft resolves the caller’s draft and checks permission.letterhead.delete on the DRAFT row, then discards it. Returns 200 with an EMPTY body (db.remove). The original letterhead is untouched.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.