File Management
Endpoints for uploading, listing, reading, writing, and deleting files within libraries.
Files can be addressed two ways: by id (/files/{fileId}) or by path (/contents/{path*}). The path-based routes create parent directories on demand and accept JSON, which makes them convenient for programmatic and AI-generated content.
POST /api/_library-upload
Upload a previously-staged file into a library.
Authentication: Required
Permissions: Requires write access to the target library (defaults to the caller's personal uploads library when libraryId is omitted)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
uploadId | string | Yes | Id of a staged upload (from the upload flow) |
libraryId | string (UUID) | No | Target library id. Omit to use the caller's uploads library |
path | string | No | File path within the library (e.g. "assets/index.js"). Creates parent directories |
progress | string | No | Progress-tracking identifier |
Response:
Uploaded files are stored as embedded with a 90-day expiresAt. For non-embedded target libraries the file is queued for vector indexing. Responds 201 Created with a Location header pointing at the new file.
{
"uploadId": "00000000-0000-4000-8000-000000000001",
"libraryId": "00000000-0000-4000-8000-000000000002"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000002/files/00000000-0000-4000-8000-000000000003"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000003",
"contentType": "text/markdown",
"directory": false,
"filename": "runbook.md",
"parentId": null,
"libraryId": "00000000-0000-4000-8000-000000000002",
"embedded": true,
"expiresAt": "2026-01-15T16:20:00.000Z",
"indexed": true,
"indexingStartedAt": null,
"ownerId": "admin",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"lobId": "16400",
"data": {},
"templateId": null,
"role": null,
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"description": null,
"assistantId": null,
"integrationId": null,
"remoteId": null,
"remoteUrl": null,
"size": 524288,
"indexedAt": "2026-01-15T16:20:00.000Z",
"indexingErroredAt": null,
"indexingErrorMessage": null,
"messageId": null,
"chatId": null
}
This route does not receive the file bytes directly. First stage the bytes through the upload flow to obtain an uploadId, then post that id here. See the Storage API for the staging step.
GET /api/libraries/{id}/files
List files under a directory with filtering, sorting, and pagination.
Authentication: Required
Permissions: Read access to the library
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
parentId | string | null | Filter by parent directory id |
dir | string | null | Filter by directory path (e.g. /docs); accepts a leading slash or not |
q | string | - | Filter by filename (case-insensitive substring) |
start | integer | 0 | Pagination offset |
end | integer | 100 | Pagination limit (max rows returned) |
sort | array | [{ colId: "filename", sort: "asc" }] | Sort model; sortable columns: filename, indexed, modifiedAt, indexingStartedAt, indexedAt, remoteSize |
dirOnly | boolean | false | Return only directories |
Response:
A flat array of the immediate children matching the filter (directories sort first). Each row carries path, canIndex (eligible for vector indexing), and runningDescendantsCount (files currently indexing beneath a directory).
[
{
"id": "00000000-0000-4000-8000-000000000001",
"filename": "docs",
"canIndex": true,
"remoteUrl": null,
"directory": true,
"contentType": "false",
"indexed": false,
"modifiedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexingStartedAt": null,
"indexedAt": null,
"path": "/docs",
"size": null,
"runningDescendantsCount": 0
},
{
"id": "00000000-0000-4000-8000-000000000002",
"filename": "README.md",
"canIndex": true,
"remoteUrl": null,
"directory": false,
"contentType": "text/markdown",
"indexed": false,
"modifiedAt": "2026-01-15T16:20:00.000Z",
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexingStartedAt": null,
"indexedAt": null,
"path": "/README.md",
"size": 524288,
"runningDescendantsCount": 0
}
]
runningDescendantsCount shows how many files within a directory are currently being indexed. Use it for progress indicators.
GET /api/libraries/{id}/files-list
Get the full, flat list of every file and directory in the library (a single non-paginated query, faster than the recursive listing above for large trees).
Authentication: Required
Permissions: Read access to the library. Non-configurers see only indexed files unless indexedOnly=false and they can configure.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
dirOnly | boolean | false | Return only directories |
indexedOnly | boolean | false | Restrict to indexed files |
Response:
A flat array ordered directories-first, then by filename.
[
{
"id": "00000000-0000-4000-8000-000000000001",
"parentId": null,
"filename": "docs",
"canIndex": true,
"remoteUrl": null,
"directory": true,
"contentType": "false",
"indexed": false,
"modifiedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexingStartedAt": null,
"indexedAt": null,
"size": null,
"libraryId": "00000000-0000-4000-8000-000000000002"
},
{
"id": "00000000-0000-4000-8000-000000000003",
"parentId": "00000000-0000-4000-8000-000000000001",
"filename": "overview.md",
"canIndex": true,
"remoteUrl": null,
"directory": false,
"contentType": "text/markdown",
"indexed": false,
"modifiedAt": "2026-01-15T16:20:00.000Z",
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexingStartedAt": null,
"indexedAt": null,
"size": "524288",
"libraryId": "00000000-0000-4000-8000-000000000002"
},
{
"id": "00000000-0000-4000-8000-000000000004",
"parentId": null,
"filename": "README.md",
"canIndex": true,
"remoteUrl": null,
"directory": false,
"contentType": "text/markdown",
"indexed": false,
"modifiedAt": "2026-01-15T16:20:00.000Z",
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexingStartedAt": null,
"indexedAt": null,
"size": "524288",
"libraryId": "00000000-0000-4000-8000-000000000002"
},
"… 1 more items (4 total) — omitted from docs"
]
PATCH /api/libraries/{id}/files
Bulk-update file metadata using JSON Patch replace operations with glob pattern matching on paths.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
Request Body:
An array of JSON Patch operations. path is a glob over file paths followed by the property to set (e.g. /docs/*.md/modifiedAt).
| Field | Type | Description |
|---|---|---|
op | string | Only replace is supported |
path | string | Glob pattern over the file path plus the trailing property |
value | any | New value for the property |
Allowed properties: filename, directory, contentType, remoteUrl, modifiedAt, indexingErroredAt, indexingErrorMessage, indexingStartedAt, indexedAt, size
Response:
[
{
"op": "replace",
"path": "/docs/*.md/modifiedAt",
"value": "2026-01-15T16:20:00.000Z"
}
]
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/files"
}
},
"status": "success"
}
indexed is intentionally not an allowed property here. To bulk update indexed status, use POST /api/libraries/{id}/files/_bulk-index instead.
GET /api/libraries/{id}/files/{fileId}
Get metadata for a specific file.
Authentication: Required
Permissions: Read access to the library
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Response:
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/files/00000000-0000-4000-8000-000000000001"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "overview.md",
"embedded": false,
"contentType": "text/markdown",
"lobId": "16400",
"role": null,
"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": "00000000-0000-4000-8000-000000000002",
"libraryId": "00000000-0000-4000-8000-000000000003",
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null,
"integrationId": null
}
PATCH /api/libraries/{id}/files/{fileId}
Edit a file's contents with exact search-and-replace edits.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
edits | array | Yes | One or more { oldText, newText } pairs |
edits[].oldText | string | Yes | Exact text to find. Must match exactly once in the file |
edits[].newText | string | No | Replacement text (empty string deletes the matched text) |
Example:
{
"edits": [
{ "oldText": "Welcome to the engineering documentation.", "newText": "Welcome to the Order Desk engineering documentation." }
]
}
Response:
Responds with the applied-edit count and a unified diff. Editing a directory returns 400.
{
"edits": [
{
"oldText": "Welcome to the engineering documentation.",
"newText": "Welcome to the Order Desk engineering documentation."
}
]
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/files/00000000-0000-4000-8000-000000000001"
}
},
"file": {
"id": "00000000-0000-4000-8000-000000000001",
"filename": "overview.md"
},
"editsApplied": 1,
"diff": "Index: overview.md\n===================================================================\n--- overview.md\toriginal\n+++ overview.md\tmodified\n@@ -1,2 +1,2 @@\n # Overview\n-Welcome to the engineering documentation.\n+Welcome to the Order Desk engineering documentation.\n"
}
This route does not accept RFC 6902 JSON Patch. It applies literal search-and-replace edits, which is the contract the AI file tools rely on. To replace a whole file, use PUT .../contents; for path-based incremental edits, use PATCH .../contents/{path*}.
DELETE /api/libraries/{id}/files/{fileId}
Delete a file from the library.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Response:
Responds 200 with an empty body.
Deleting a directory also deletes every file and subdirectory within it.
GET /api/libraries/{id}/files/{fileId}/contents
Get file contents as a stream. Served inline by default; pass ?download=true for a Content-Disposition: attachment download.
Authentication: Required
Permissions: Read access to the library and file
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
download | boolean | When true, download instead of inline |
Response:
The raw file bytes with the file's Content-Type. (Binary stream, not shown.)
PUT /api/libraries/{id}/files/{fileId}/contents
Replace a file's contents.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Request Body:
The raw new file content (the request body is the file bytes, not a JSON envelope).
Response:
Responds with the updated file. Writing to a directory returns 400.
"# Overview\nReplaced contents.\n"
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/files/00000000-0000-4000-8000-000000000001/contents"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "overview.md",
"embedded": false,
"contentType": "text/markdown",
"lobId": "16400",
"role": null,
"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": "00000000-0000-4000-8000-000000000002",
"libraryId": "00000000-0000-4000-8000-000000000003",
"remoteId": null,
"remoteUrl": null,
"size": 524288,
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null,
"integrationId": null
}
GET /api/libraries/{id}/contents/{path*}
Read file contents by path (supports nested paths). This addresses a library file by path instead of id and serves identically to GET .../files/{fileId}/contents.
Authentication: Required
Permissions: Read access to the library and file
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
path | string | File path (e.g. assets/index.js) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
download | boolean | When true, download instead of inline |
Response:
The raw file bytes with the file's Content-Type. (Binary stream, not shown.)
Example:
GET /api/libraries/admin:engineering-docs/contents/assets/images/logo.png
PUT /api/libraries/{id}/contents/{path*}
Write file contents by path. Creates the file and any missing parent directories.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
path | string | File path (e.g. docs/new-file.md) |
Request Body:
JSON, not raw bytes:
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | File content (may be empty) |
contentType | string | No | MIME type (auto-detected from the filename when omitted) |
encoding | string | No | utf8 (default) or base64 |
Response:
Responds 201 Created when the file is new, 200 OK when it already existed.
{
"content": "# Overview\nWelcome to the engineering documentation.\n"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/contents/docs/overview.md"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"embedded": false,
"directory": false,
"libraryId": "00000000-0000-4000-8000-000000000002",
"parentId": "00000000-0000-4000-8000-000000000003",
"filename": "overview.md",
"contentType": "text/markdown",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"lobId": "16400",
"ownerId": null,
"data": {},
"templateId": null,
"role": null,
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"description": null,
"assistantId": null,
"integrationId": null,
"remoteId": null,
"remoteUrl": null,
"size": 524288,
"indexed": false,
"indexingStartedAt": null,
"indexedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"messageId": null,
"chatId": null,
"expiresAt": null
}
PATCH /api/libraries/{id}/contents/{path*}
Incrementally update file contents by path.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
path | string | File path |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | One of replace, append, prepend, insert |
search | string | replace | Text to find (replace) |
replacement | string | No | Replacement text (replace) |
replaceAll | boolean | No | Replace all occurrences instead of the first (replace) |
text | string | append/prepend/insert | Text to add |
insertAt | integer | insert | Character offset for insert |
Example:
{ "operation": "append", "text": "\n## Troubleshooting\nSee the runbook.\n" }
Response:
Responds with the updated file. The file must already exist (404 otherwise); patching a directory returns 400.
{
"operation": "append",
"text": "\n## Troubleshooting\nSee the runbook.\n"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/contents/docs/setup.md"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "setup.md",
"embedded": false,
"contentType": "text/markdown",
"lobId": "16400",
"role": null,
"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": "00000000-0000-4000-8000-000000000002",
"libraryId": "00000000-0000-4000-8000-000000000003",
"remoteId": null,
"remoteUrl": null,
"size": 524288,
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null,
"integrationId": null
}
POST /api/libraries/{id}/files/_bulk-index
Opt multiple files into or out of vector indexing.
Authentication: Required
Permissions: Requires strict configure access on the library
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
files | array | Yes | File ids (UUIDs) to affect |
indexed | boolean | No | Mark them indexed (true, default) or not |
Example:
{ "files": ["00000000-0000-4000-8000-000000000001"], "indexed": true }
Response:
Opting in schedules the index-files job; database triggers cascade the change through directory subtrees and manage embeddings. Responds 200 with an empty body.
{
"files": [
"00000000-0000-4000-8000-000000000001"
],
"indexed": true
}
GET /api/libraries/{id}/files/{fileId}/indexed
Get the current indexed flag of a file.
Authentication: Required
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Response:
A bare boolean.
false
PUT /api/libraries/{id}/files/{fileId}/indexed
Opt a file into indexing.
Authentication: Required
Permissions: Requires strict configure access on the library
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Request Body:
None. The route always sets indexed: true and schedules the index-files job; for a directory the change cascades to its subtree.
Response:
Responds with the updated file.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/files/00000000-0000-4000-8000-000000000001/indexed"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "overview.md",
"embedded": false,
"contentType": "text/markdown",
"lobId": "16400",
"role": null,
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"directory": false,
"indexed": true,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": "00000000-0000-4000-8000-000000000002",
"libraryId": "00000000-0000-4000-8000-000000000003",
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null,
"integrationId": null
}
DELETE /api/libraries/{id}/files/{fileId}/indexed
Opt a file out of indexing and delete its embeddings.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Response:
Responds 200 with an empty body. A database trigger clears indexedAt / indexingErroredAt and removes embeddings.
POST /api/libraries/{id}/files/{fileId}/_retry-indexing
Clear a file's indexing error and re-schedule indexing.
Authentication: Required
Permissions: Requires library:write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Library id or natural id |
fileId | string (UUID) | File id |
Response:
Responds with the updated file.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/libraries/admin%3Aengineering-docs/files/00000000-0000-4000-8000-000000000001/_retry-indexing"
}
},
"extension": "md",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "overview.md",
"embedded": false,
"contentType": "text/markdown",
"lobId": "16400",
"role": null,
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"directory": false,
"indexed": true,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": "2026-01-15T16:20:00.000Z",
"parentId": "00000000-0000-4000-8000-000000000002",
"libraryId": "00000000-0000-4000-8000-000000000003",
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null,
"integrationId": null
}
Use this when indexingErroredAt is set and you want to retry after fixing the underlying issue.