Files
Endpoints for managing the files attached to a Template: the TEMPLATE source
file, ASSET files (CSS, JS, images), and DATA files.
Every route on this page gates on the templates license feature. The pre-block
chain asserts that feature first (template.ensureTemplates calls
req.assertFeature('templates')), then runs template.lookup(params.id) via the
read_access scope, so a missing or unreadable Template answers 404 before the
handler runs. Mutating routes add a permission check after the lookup:
permission.template.write for POST/PUT, and permission.template.edit for
DELETE.
A File row carries metadata only. Its bytes live separately and are read or
written through the inf:file-contents link, never through these metadata
endpoints.
GET /api/templates/{id}/files
List the files whose templateId is this Template.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id)
Response:
A HAL collection with the standard start/count/total envelope, embedding
inf:file. Each item adds an inf:file-contents sub-link to its byte stream. The
collection covers all roles: TEMPLATE source files, ASSET files, and DATA
files.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files"
}
},
"start": 0,
"count": 3,
"total": 3,
"_embedded": {
"inf:file": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002/contents"
}
},
"extension": "txt",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"filename": "scratch.txt",
"embedded": true,
"contentType": "text/plain",
"lobId": null,
"role": "ASSET",
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"integrationId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": "00000000-0000-4000-8000-000000000001"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000003"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000003/contents"
}
},
"extension": "njk",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000003",
"filename": "invoice.njk",
"embedded": true,
"contentType": "text/html",
"lobId": "16400",
"role": "TEMPLATE",
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"integrationId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": "00000000-0000-4000-8000-000000000001"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000004"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000004/contents"
}
},
"extension": "css",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000004",
"filename": "styles.css",
"embedded": true,
"contentType": "text/css",
"lobId": "16400",
"role": "ASSET",
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"integrationId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": "00000000-0000-4000-8000-000000000001"
}
]
}
}
File properties:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | File id |
filename | string | Filename with extension |
extension | string | Derived file extension |
role | string | TEMPLATE, ASSET, or DATA |
contentType | string | MIME type |
size | string | null | Byte size as a string, or null when no contents are stored |
templateId | string (UUID) | Owning Template id |
embedded | boolean | Always true for Template files |
lobId | string | null | Internal large-object id backing the contents, or null |
data | object | Driver-specific metadata |
createdAt / updatedAt | date | Timestamps |
POST /api/templates/{id}/files
Create a new file for the Template.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.write(pre.template)
Request Body:
The payload validates only the three fields below. The handler forces
embedded: true and takes templateId from the URL, so neither can be set by the
caller.
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | No | Filename with extension |
contentType | string | No | MIME type |
role | string | No | ASSET, DATA, or TEMPLATE |
Example Request:
{
"filename": "footer.html",
"contentType": "text/html",
"role": "ASSET"
}
Response:
Responds 201 Created with the new File and a Location header pointing at the
file lookup. This creates metadata only; no contents are written. Upload the bytes
separately through the contents PUT.
{
"filename": "footer.html",
"contentType": "text/html",
"role": "ASSET"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002"
}
},
"extension": "html",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"directory": false,
"filename": "footer.html",
"contentType": "text/html",
"role": "ASSET",
"templateId": "00000000-0000-4000-8000-000000000001",
"embedded": true,
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"lobId": null,
"ownerId": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"description": null,
"assistantId": null,
"integrationId": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"modifiedAt": null,
"indexed": false,
"indexingStartedAt": null,
"indexedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"messageId": null,
"chatId": null,
"expiresAt": null
}
POST /api/templates/{id}/files/_upload
Upload a file to the Template as multipart form data.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.write(pre.template)
Request:
Multipart form data carrying the file bytes.
Response:
Responds 201 Created with the stored File.
The request body is raw multipart bytes, so this endpoint is not captured here.
GET /api/templates/{id}/files/{file}
Get the metadata for a single Template file.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id)
Response:
The File's metadata plus an inf:file-contents link. A file id that does not
belong to this Template answers 404.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002/contents"
}
},
"extension": "html",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"filename": "footer.html",
"embedded": true,
"contentType": "text/html",
"lobId": null,
"role": "ASSET",
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": "00000000-0000-4000-8000-000000000001",
"integrationId": null
}
PUT /api/templates/{id}/files/{file}
Update a Template file's metadata.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.write(pre.template)
Request Body:
This route has no Joi payload schema. The body is whatever File columns you want to
change, typically filename and contentType.
| Field | Type | Description |
|---|---|---|
filename | string | New filename |
contentType | string | MIME type |
Example Request:
{
"filename": "page-footer.html",
"contentType": "text/html"
}
Response:
Responds 200 with the updated File.
{
"filename": "page-footer.html",
"contentType": "text/html"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002/contents"
}
},
"extension": "html",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"filename": "page-footer.html",
"embedded": true,
"contentType": "text/html",
"lobId": null,
"role": "ASSET",
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": "00000000-0000-4000-8000-000000000001",
"integrationId": null
}
A pre-block (internals.checkRename) returns 403 if you try to set filename on
a file whose role is TEMPLATE. Other roles rename freely.
DELETE /api/templates/{id}/files/{file}
Delete a file from the Template.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.edit(pre.template)
Response:
Responds 200 with an empty body (Informer convention), not 204.
Deleting the file that is currently the primary TEMPLATE file also nullifies the
Template's templateFileId.
GET /api/templates/{id}/files/{file}/contents
Get the byte contents of a Template file.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id)
Response:
The file bytes with an appropriate Content-Type header.
This endpoint streams raw file bytes rather than JSON, so it is not captured here.
PUT /api/templates/{id}/files/{file}/contents
Replace the byte contents of a Template file.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.write(pre.template)
Request Body:
Raw file contents (text or binary, depending on the content type).
Response:
Confirms the contents were written.
The request body is a raw byte stream, so this endpoint is not captured here.
GET /api/templates/{id}/template-file
Get the primary Template file's metadata.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id)
Response:
The File whose id equals the Template's templateFileId (the primary TEMPLATE
source), plus an inf:file-contents link.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/template-file"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/template-file/contents"
}
},
"extension": "njk",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"filename": "invoice.njk",
"embedded": true,
"contentType": "text/html",
"lobId": "16400",
"role": "TEMPLATE",
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": "00000000-0000-4000-8000-000000000001",
"integrationId": null
}
PUT /api/templates/{id}/template-file
Set the primary Template file.
Authentication: Required
Pre-blocks: template.ensureTemplates, template.lookup(params.id), permission.template.write(pre.template)
Request Body:
This route has no Joi payload schema. It does not accept a { templateFileId }
selector. Instead, it creates a new File from the request body (forced to
role: 'TEMPLATE' and embedded: true) and repoints the Template's
templateFileId at it.
| Field | Type | Description |
|---|---|---|
filename | string | Filename for the new primary file |
contentType | string | MIME type |
Example Request:
{
"filename": "invoice-v2.njk",
"contentType": "text/html"
}
Response:
Responds 200 with the newly created File. The Template's templateFileId now
points at it.
{
"filename": "invoice-v2.njk",
"contentType": "text/html"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/template-file"
},
"inf:file-contents": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002/contents"
}
},
"extension": "njk",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"directory": false,
"filename": "invoice-v2.njk",
"contentType": "text/html",
"templateId": "00000000-0000-4000-8000-000000000001",
"embedded": true,
"role": "TEMPLATE",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"lobId": null,
"ownerId": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"description": null,
"assistantId": null,
"integrationId": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"modifiedAt": null,
"indexed": false,
"indexingStartedAt": null,
"indexedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"messageId": null,
"chatId": null,
"expiresAt": null
}