Storage
Endpoints for working with standalone files in Informer storage: upload, single
file metadata lookup, raw download, and filename search. All routes live under
the /api prefix.
The Storage module registers four routes, and all four require a superuser.
Files belong to Libraries, chats, Jobs, letterheads and Assistants, each with its
own read rules, and these routes reach any file in the Tenant directly by id or by
name, whatever owns it. Everyone else reaches a file through its owner's API, for
example
GET /api/libraries/{id}/files/{fileId}/contents.
A caller who is not a superuser gets 403 Forbidden from every route on this
page, whether they use a session or an API token.
POST /api/files
Upload a new file via multipart form data.
Authentication: Required (session or API token), superuser only
Request: multipart/form-data with a single file part streamed to the
server.
| Form field | Type | Required | Description |
|---|---|---|---|
file | file (stream) | Yes | The file to upload |
Example Request:
curl -X POST https://informer.example.com/api/files \
-H "Authorization: Bearer <token>" \
-F "file=@monthly-report.pdf"
The handler writes the uploaded bytes into a Postgres large object and replies
201 Created with the full File resource and a Location header pointing at
the new file's lookup route (/api/files/{id}).
This is a binary upload, so it has no captured example. The request body is a
raw multipart buffer rather than a documentable JSON payload. The response is
the same File HAL resource shown under GET /api/files/{id} below.
GET /api/files/{id}
Look up a single file's metadata.
Authentication: Required (session or API token), superuser only
Pre-blocks: auth.assertSuperuser
Looks the file up by id anywhere in the Tenant, whatever owns it. The response
is a single HAL resource with the file's metadata and a self link.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/files/00000000-0000-4000-8000-000000000001"
}
},
"extension": "pdf",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "monthly-report.pdf",
"embedded": false,
"contentType": "application/pdf",
"lobId": "16400",
"role": null,
"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": "admin",
"templateId": null
}
File properties:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | File id |
filename | string | Stored filename |
extension | string | Filename extension, derived from filename |
contentType | string | MIME type |
size | string | File size in bytes (serialized as a string) |
lobId | string | Postgres large-object id holding the bytes |
embedded | boolean | Whether the file is embedded in another entity |
role | string | null | File role for embedded/template files (e.g. template asset); null for standalone files |
directory | boolean | Whether this row represents a directory |
indexed | boolean | Whether the file's contents have been indexed |
data | object | Driver/metadata bag |
ownerId | string | Owning user's username |
templateId | string | null | Associated Template id when the file belongs to a Template |
description | string | null | Optional description |
createdAt / updatedAt | date | Timestamps |
The model also serializes association ids such as parentId, libraryId,
assistantId, chatId, messageId, integrationId, oauthClientId,
letterheadId, and jobActionId/jobActionAttachmentId, all null for a
standalone file. There is no datasetId column; a file's Dataset association is
tracked through other columns.
For a superuser, an id that does not exist in the Tenant answers 404 rather
than an empty body. Anyone else gets 403 before the lookup runs, so the route
never reveals whether an id exists.
{
"statusCode": 404,
"error": "Not Found",
"message": "Not Found"
}
GET /api/files/{id}/download
Download the raw file contents.
Authentication: Required (session or API token), superuser only
Pre-blocks: auth.assertSuperuser, then file.lookup(params.id)
Streams the raw file bytes through the shared file.getContents handler. The
response carries the file's Content-Type and a
Content-Disposition: attachment header so browsers download rather than render
it.
This is a binary response, so it has no captured example. A file with no stored
content short-circuits to 204 No Content.
GET /api/files
Search files in the Tenant by filename.
Authentication: Required (session or API token), superuser only
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | - | Partial, case-insensitive filename match (ILIKE) |
start | integer | 0 | Paging offset |
limit | integer | - | Page size |
sort | string | - | Sort field |
The search query param is q (a partial filename match), not filename. The
route is the standard db.query HAL collection over the File model and returns
the paged-collection envelope (start, limit, count, total) with file
rows under _embedded["inf:file"]. Each embedded file also embeds its
inf:owner (resolved through user.lookup).
The File model defines no default sort, so rows come back in database order. The captured example below was sorted by filename for a stable byte-identical capture; the route itself does not impose an ORDER BY.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/files"
}
},
"start": 0,
"count": 0,
"total": 0,
"_embedded": {
"inf:file": []
}
}
Notes
- Files can be standalone or embedded in another entity (Templates, and so on). Embedded files are managed through their parent entity's API.
- File upload supports streaming for large files; bytes are stored in a Postgres large object.
- Downloaded files include the original filename in the
Content-Dispositionheader.