Skip to main content

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.

Superusers only

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 fieldTypeRequiredDescription
filefile (stream)YesThe 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}).

Not shown

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.

Get file metadataGET /api/files/00000000-0000-4000-8000-000000000001
GET /api/files/{id} looks the file up by id anywhere in the tenant, whatever owns it, and returns its metadata as a single HAL resource. Only a superuser may call it; anyone else gets 403. An unknown id returns 404. The response includes filename, contentType, size (bytes), embedded, role, createdAt/updatedAt, and a self link.
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

File properties:

FieldTypeDescription
idstring (UUID)File id
filenamestringStored filename
extensionstringFilename extension, derived from filename
contentTypestringMIME type
sizestringFile size in bytes (serialized as a string)
lobIdstringPostgres large-object id holding the bytes
embeddedbooleanWhether the file is embedded in another entity
rolestring | nullFile role for embedded/template files (e.g. template asset); null for standalone files
directorybooleanWhether this row represents a directory
indexedbooleanWhether the file's contents have been indexed
dataobjectDriver/metadata bag
ownerIdstringOwning user's username
templateIdstring | nullAssociated Template id when the file belongs to a Template
descriptionstring | nullOptional description
createdAt / updatedAtdateTimestamps

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.

Missing file

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.

Get a file that does not existGET /api/files/00000000-0000-4000-8000-000000000001
GET /api/files/{id} answers a superuser 404 when no file with that id exists in the tenant, rather than an empty body. Anyone else gets 403 before the lookup runs.
Response · 404
{
"statusCode": 404,
"error": "Not Found",
"message": "Not Found"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Not shown

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:

ParameterTypeDefaultDescription
qstring-Partial, case-insensitive filename match (ILIKE)
startinteger0Paging offset
limitinteger-Page size
sortstring-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.

Search files by filenameGET /api/files?q=report
GET /api/files is the db.query HAL collection over the File model. The search query param is `q` (a partial, case-insensitive ILIKE match against the filename), not a `filename` param. It returns the standard paged envelope (start, limit, count, total) with file rows embedded under `_embedded["inf:file"]`; each row also embeds its `inf:owner`. Standard paging params (start, limit, sort) are accepted. Only a superuser may search, since it matches every file in the tenant whatever owns it; anyone else gets 403. Rows are ordered by filename in this example for stability; the route itself returns DB order.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/files"
}
},
"start": 0,
"count": 0,
"total": 0,
"_embedded": {
"inf:file": []
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

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-Disposition header.