AI & Chat RAG-powered chat, semantic search, notes, metadata extraction, and auto-summarization
AI agent endpoint paths (/ai/agent/): the AI agent endpoints are served under /ai/agent/, which is the canonical path family. The older /ai/chat/ paths remain a legacy alias — every agent endpoint still resolves through the /ai/chat/ anchor to the exact same handler, so an older integration calling /ai/chat/... continues to work unchanged.
New integrations should use /ai/agent/.... If you migrate an existing /ai/chat/... integration, swap only the URL family — the request parameters, response shapes, and behavior are identical between the two anchors because they reach the same code.
Overview
Fastio provides built-in AI capabilities for workspaces and shares:
- RAG-powered chat — Ask questions about indexed files with citations to specific pages and snippets
- General chat — AI conversation with optional file attachments for one-off analysis
- Auto-summarization — AI-generated titles and descriptions for shares
- Metadata extraction — AI-powered structured metadata extraction from documents, spreadsheets, images, and code
- Semantic search — Find files by meaning, not just keywords
- Notes — Markdown storage nodes that are indexed for RAG, letting you bank knowledge over time
Fastio's AI is a full agent, not a read-only Q&A tool. In addition to reading, analyzing, searching, and answering questions about your files, it can take actions on your behalf — for example, creating documents and notes and organizing your content. It always acts within your own permissions and plan entitlements, and its actions consume credits like any other operation.
Plan Requirements
AI capabilities are gated by a set of billing-plan features:
content_ai— master switch for AI features. Covers the baseline AI surfaces: listing chats, reading chat details and message history, streaming existing responses, and notes. Available on all paid plans.ai_agent— narrower gate for interactive agentic flows. Required to create chats, send chat messages, and enable theintelligencetoggle on a workspace or portal share (the toggle controls the RAG indexing pipeline that feeds the chat surface).ai_autotitle— gates the share auto-title / description endpoint (POST /share/{id}/ai/autotitle/). A plan without this feature is rejected when calling that endpoint.ai_autoog— gates the AI-generated OG image for private shares (GET /share/{id}/ai/autoog/). A private share on a plan without this feature is served the default private OG image instead of a custom one; public-share OG generation does not require it.
Plan coverage:
| Plan | content_ai | ai_agent | Effect |
|---|---|---|---|
| Starter | on | on | Full AI (chat, intelligence, RAG) |
| Business | on | on | Full AI (chat, intelligence, RAG) |
| Growth | on | on | Full AI (chat, intelligence, RAG) |
All current paid plans (Starter, Business, and Growth) include both content_ai and ai_agent, so full AI — chat, intelligence, and RAG — is available on every plan a new organization can choose. New organizations choose one of these paid plans.
How gated endpoints respond when a plan lacks the feature:
- Create chat / send message:
1695 (Upgrade Required)→ HTTP 402 - Attempt to set
intelligence=trueon create/update:1605 (Invalid Input)with a message indicating intelligence requires a plan that supports agentic AI
New organizations — for humans and AI agents alike — choose a paid plan (Starter, Business, or Growth), each of which includes both content_ai and ai_agent and unlocks full agentic chat, intelligence, and RAG. See the Organizations reference for plan selection.
Intelligence Setting
The intelligence boolean on a workspace or portal share controls whether uploaded files are automatically indexed for RAG.
intelligence=true— Files are auto-indexed for semantic search, summarization, and citation. Required for the agent to search the scope’s indexed files (RAG) and cite them. Requires bothcontent_aiandai_agentplan features (see Plan Requirements above). On plans that includeai_agent, new workspaces created by agent accounts defaultintelligencetotrue. If a plan does not includeai_agent, the default isfalsebecause the indexing pipeline has no consumer.intelligence=false— Files are stored/shared without RAG indexing. You can still chat with the agent and attach specific file references for direct file analysis (when the plan supports chat).
Shared folder restriction: Intelligence is only available on portal shares (independent storage). Workspace folder shares (
storage_mode=workspace_folder) cannot have intelligence enabled — their files are indexed through the parent workspace instead. The API will return an error if you attempt to enable intelligence on a shared folder share.
Enable at creation:
/current/org/{org_id}/create/workspace/
Pass intelligence=true
Update later:
/current/workspace/{workspace_id}/update/
Pass intelligence=true
Note: Intelligence can be enabled and disabled within time restrictions. Disabling intelligence destroys indexed embeddings (the vector index is flushed). Re-enabling intelligence incurs re-indexing costs as AI credits are consumed to re-index all files. When a plan loses
ai_agent(e.g. on downgrade), the RAG indexing pipeline stops even if the instance flag remains set; previously indexed embeddings remain but will not be updated.
How the Agent Uses Files
There is no chat “type” to choose — a single agent surface handles every conversation. You do not pass a type (or personality) parameter; the agent adapts to what you send:
- General conversation. Ask anything; the agent answers from its own knowledge. No files are required.
- Grounded in indexed files (RAG). When the workspace/share
intelligencesetting is enabled, the agent can search the scope’s indexed files and return answers with citations to specific files, pages, and text snippets. With no attachments it may draw on the entire indexed scope; only files that reachai_state: indexedparticipate in that search. - Focused on specific files/folders. Attach files or folders to a turn by including them as reference items in the
references,content_parts,subjects, oruploadsarrays (see Attaching Files and Folders below). Any file with a ready preview or AI summary is eligible; the agent uses the attached content directly (e.g., “Describe this image”, “Summarize this PDF”).
A single agent turn can combine all of the above — general reasoning, RAG over the indexed scope, and directly attached files.
Attaching Files and Folders
Give the agent specific files or folders as context by including them as reference items in the references, content_parts, or subjects array of a create-chat or send-message request. You do not assemble a file’s metadata yourself — send only the node id (and, for a file, an optional version id), and Fastio resolves the full details server-side after verifying your access and the file’s AI-readiness.
subjects pins objects from inside the workspace/share as the turn’s focus; a separate uploads array carries files staged from outside the scope. content_parts is an ordered stream that interleaves text segments with inline reference pills (the same item shape). All three of references, content_parts, and subjects are resolved and gated by the rules below.
File reference
{ "type": "file", "id": "{node_id}", "file_details": { "node_id": "{node_id}", "version_id": "{version_id}" } }
version_idis optional — omit it to attach the file’s current version. Get aversion_idfrom the file’sversionfield in a storage list/details response.- Only file (or note) nodes may be attached as
type: file. - The file must be AI-eligible (have a ready preview or summary).
Folder reference
{ "type": "folder", "id": "{node_id}", "folder_details": { "node_id": "{node_id}" } }
- Attaches a folder so the agent can use its contents as context. Only folder nodes may be attached as
type: folder. - The folder’s indexed files participate in the agent’s RAG search (requires
intelligenceenabled).
Limits
- Up to 20 files and 200 MB total per turn (large images and videos count at their smaller preview size).
- Up to 100 file/folder references total across all three arrays.
Validation is strict — a bad reference fails the request
If a referenced file or folder cannot be attached — it does not exist, you cannot access it, it has been deleted, or it is not AI-eligible — the request is rejected with an error, not silently dropped:
1609 (Not Found)/ 404 — the referenced file or folder does not exist, is not accessible, has been deleted, or a pinned version is unavailable (all reported the same way so a caller cannot probe existence).1605 (Invalid Input)/ 406 — a reference is malformed, is missing its node id, carries an invalid/conflicting version id, is the wrong node type (a folder referenced as a file, or vice-versa), or the request exceeds the 20-file / 200 MB / 100-reference / 200-occurrence limits.1680 (Access Denied)/ 401 — folder attachments are not permitted in this share (a restricted-view guest). Does not occur on workspace chats, where members can view all files.
Choosing what to attach
| Use Case | What to attach |
|---|---|
| Analyze specific files directly | File reference items in references / content_parts / subjects |
| Ground answers in a folder’s indexed files (with citations) | Folder reference items (requires intelligence enabled) |
| Ask general questions across all indexed files | Nothing — the agent may search the whole indexed scope |
| General conversation, no files | Nothing |
AI State (File Readiness)
Files in an intelligent workspace progress through AI processing states:
| State | Meaning |
|---|---|
disabled | Intelligence not enabled for this file/workspace |
pending | Queued for AI processing |
in_progress | Currently being processed by AI |
ready | File can be used with AI chat (attached directly or via scope). The file has been processed enough (e.g., preview/summary generated) to be usable in AI conversations. |
indexed | File contents (for documents) have been indexed via RAG. This state is used when intelligence is enabled on the workspace/share. Indexed files are searchable by semantic meaning and their content is used as grounding in scoped AI chats. |
failed | AI processing failed |
Files with ai_state: ready can be used with AI chat. Files with ai_state: indexed have additionally had their contents indexed for RAG-powered semantic search. When intelligence is enabled on a workspace/share, files progress to indexed automatically. Check a file’s AI state in the ai.state field of storage list or file details responses.
Controlling Response Length and Style
There is no personality parameter. Control verbosity and style directly in your question phrasing:
- “In one sentence, what is the main conclusion?”
- “List only the file names that mention GDPR, no explanations”
- “Give me a brief summary — 2–3 bullet points max”
Advanced Per-Turn Fields
Beyond question and the file-reference arrays, a create-chat or send-message request accepts these optional per-turn fields. All are optional; omit them for normal use.
| Field | Type | Description |
|---|---|---|
uploads | JSON array | Focus files staged from outside the workspace/share (as opposed to subjects, which pins objects from inside it). |
view | JSON object | A snapshot of the caller’s current UI view, so the agent can reason about what the user is looking at. |
activity | JSON array | Recent-activity entries giving the agent short-term context. |
role_in_org | string | The acting user’s free-text role in the organization, used to tailor the response. |
idempotency_key | string | Client-supplied replay guard (max 64 chars). Re-sending the same key returns the already-created turn instead of creating a duplicate. Omit to have one generated. |
The acting user’s identity is taken from your Bearer token — it is never read from the request body, so a request cannot spoof who the turn runs as.
Notes (Stored Knowledge)
Notes are a storage node type (like files and folders) that store markdown content directly on the server. They appear in storage listings with "type": "note" and "mimetype": "text/markdown". Notes are workspace-only — they cannot be created in shares.
Why Notes Matter
In an intelligent workspace, notes are ingested and indexed just like uploaded files. This makes them a way to bank knowledge over time — store interesting facts, research findings, decisions, or project context. In future AI chats that scope the entire workspace (or include the note’s folder), the note content will be used as grounding when the AI searches for relevant information.
Create a note
/current/workspace/{workspace_id}/storage/{parent_id}/createnote/
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Note name, must end in .md |
| content | string | Yes | Markdown content, max 100 KB |
{parent_id} is a folder OpaqueId or "root". Returns the created note as a node resource.
Update a note
/current/workspace/{workspace_id}/storage/{node_id}/updatenote/
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | No | New name, must end in .md |
| content | string | No | New markdown content, max 100 KB, non-blank (an empty or whitespace-only value is rejected) |
At least one of name or content must be provided. Updating content creates a new version.
Read note content
/current/workspace/{workspace_id}/storage/{node_id}/read/
Returns the raw markdown content.
Linking a user to a note
- In workspace context:
https://{org_domain}.fast.io/workspace/{workspace_name}?note={note_opaque_id} - Direct preview: use the preview URL for the note node
Workspace AI Endpoints
Create a new chat
/current/workspace/{workspace_id}/ai/agent/
Creates a thread and its first turn; the AI processes it asynchronously. There is no type or personality parameter — the request body is the initial question plus optional file references and the thread create-time fields.
Auth: Bearer token required. Workspace view permission. content_ai and ai_agent plan features required.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| question | string | Yes | — | Initial question, 1–32,000 characters. (May be omitted only when content_parts carries the message text.) |
| privacy | string | No | private | private or public. public is currently disabled platform-wide — a privacy=public request returns 403 Forbidden; see “Publish a private chat” below. |
| name | string | No | Auto-generated | Chat name. A default is used if omitted. |
| kind | string | No | user | user or agent. agent flags the chat as agentic. Set at creation, immutable thereafter. |
| references | JSON array | No | — | File/folder reference items to attach as context — each a {type, id} file or folder item (see Attaching Files and Folders). Up to 20 files / 200 MB / 100 references; the backend resolves each item’s full details server-side. |
| content_parts | JSON array | No | — | Ordered content stream — text segments plus inline file/folder reference pills (same item shape as references). |
| subjects | JSON array | No | — | File/folder reference items pinned as focus subjects for the turn (same item shape as references). |
| uploads | JSON array | No | — | Focus files staged from outside the workspace. |
Also accepts the optional view, activity, role_in_org, and idempotency_key fields (see Advanced Per-Turn Fields above).
Request example:
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/" \
-H "Authorization: Bearer {jwt_token}" \
-d "question=What were the Q3 revenue figures?" \
-d "privacy=private"
Response (200 OK):
{
"result": true,
"thread": {
"thread_id": "aBcDeFgHiJkLmNoPqR",
"creator": { "type": "user", "id": "1234567890123456789" },
"scope": { "type": "workspace", "id": "1234567890123456789" },
"name": "New Chat",
"status": "active",
"kind": "user",
"cost": { "credits": 0, "tokens": 0 },
"privacy": { "visibility": "private", "owner": { "type": "user", "id": "1234567890123456789" } },
"created_at": "2026-07-07 16:37:29 UTC",
"updated_at": "2026-07-07 16:37:29 UTC"
},
"turn": {
"turn_id": "xYzAbCdEfGhIjKlMnO",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 1,
"status": "pending",
"idempotency_key": "3f2a…",
"query": { "text": "What were the Q3 revenue figures?" },
"error": null,
"cost": { "credits": 0, "tokens": 0 },
"created_at": "2026-07-07 16:37:29 UTC",
"updated_at": "2026-07-07 16:37:29 UTC"
}
}
| Field | Type | Description |
|---|---|---|
thread.thread_id | string | Opaque ID of the created thread (the chat). Use it as {chat_id} in the follow-up URLs below. |
turn.turn_id | string | Opaque ID of the initial turn (the first message). Use it as the {message_id} in the message-details / read URLs. |
turn.status | string | Initial turn status — pending. The AI processes it asynchronously (see turn statuses under “Get message details”). |
The full field lists are in Chat Session Object Schema (thread) and Message Object Schema (turn) below.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Invalid privacy, kind, or name, or invalid question length |
1609 (Not Found) | 404 | An attached file or folder reference does not exist or is not accessible |
1605 (Invalid Input) | 406 | An attached reference is malformed, the wrong node type, or exceeds the 20-file / 200 MB / 100-reference limit |
1700 (Forbidden) | 403 | privacy=public requested while public chats are disabled platform-wide |
1660 (Conflict) | 409 | Thread still committing its first turn (retry with the same idempotency key), or the first message is too large to process |
1664 (Datastore Error) | 500 | Thread or turn creation failed |
List chats
/current/workspace/{workspace_id}/ai/agent/list/
Returns all chats created by the current user in the workspace. Sorted by most recently modified first.
Auth: Bearer token required. Workspace view permission. content_ai plan feature required.
Query parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| kind | string | No | user | Filter by chat kind. Allowed values: user (only user-driven chats — the historical default), agent (only agentic chats), all (user + agent chats). Omit or pass user for backwards-compatible behavior. |
Variant: Append /deleted to the path to list deleted chats: GET .../ai/agent/list/deleted
Request example:
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/list/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"chats": {
"count": 1,
"items": [
{
"thread_id": "aBcDeFgHiJkLmNoPqR",
"creator": { "type": "user", "id": "1234567890123456789" },
"scope": { "type": "workspace", "id": "1234567890123456789" },
"name": "Quarterly report analysis",
"status": "active",
"kind": "user",
"cost": { "credits": 15, "tokens": 1500 },
"privacy": {
"visibility": "private",
"owner": { "type": "user", "id": "1234567890123456789" }
},
"created_at": "2026-07-07 16:00:00 UTC",
"updated_at": "2026-07-07 16:30:05 UTC",
"message_count": 5,
"continuable": true,
"latest_message": {
"turn_id": "xYzAbCdEfGhIjKlMnO",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 5,
"status": "complete",
"idempotency_key": "3f2a…",
"query": { "text": "Summarize the quarterly report" },
"error": null,
"cost": { "credits": 3, "tokens": 300 },
"created_at": "2026-07-07 16:30:00 UTC",
"updated_at": "2026-07-07 16:30:05 UTC"
}
}
]
}
}
| Field | Type | Description |
|---|---|---|
chats | object | Collection envelope {count, items} |
chats.count | integer | Number of chat items returned in items |
chats.items | array | Array of thread (chat) objects |
chats.items[].thread_id | string | Opaque ID of the thread (the chat) |
chats.items[].creator | object | {type, id} — the chat creator |
chats.items[].scope | object | {type, id} — the workspace or share the chat lives in |
chats.items[].name | string | Chat display name |
chats.items[].status | string | Chat status |
chats.items[].kind | string | user or agent. Always present; chats created before the field existed default to user. |
chats.items[].message_count | integer | Total turns (messages) in the chat |
chats.items[].continuable | boolean | true if the chat can be continued with a new message; false if the chat has no resumable conversation state (read-only history, e.g. a legacy chat migrated for history only). Omitted on create/update responses. |
chats.items[].latest_message | object/null | Most recent turn (see Message Object Schema), or null for an empty thread |
chats.items[].cost.credits | integer | Credit charge for the chat (raw tokens converted at the meter rate) |
chats.items[].cost.tokens | integer | Raw token consumption the credit charge derives from |
chats.items[].privacy | object | {visibility, owner} |
chats.items[].created_at | string | Creation timestamp (YYYY-MM-DD HH:MM:SS UTC) |
chats.items[].updated_at | string | Last update timestamp (YYYY-MM-DD HH:MM:SS UTC) |
Get chat details
/current/workspace/{workspace_id}/ai/agent/{chat_id}/details/
Returns chat details with full message history.
Auth: Bearer token required. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/details/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
The response is a thread object plus a separate turns collection (the message history) — there is no chat object and no embedded messages array.
{
"result": true,
"thread": {
"thread_id": "aBcDeFgHiJkLmNoPqR",
"creator": { "type": "user", "id": "1234567890123456789" },
"scope": { "type": "workspace", "id": "1234567890123456789" },
"name": "Quarterly report analysis",
"status": "active",
"kind": "user",
"cost": { "credits": 15, "tokens": 1500 },
"privacy": { "visibility": "private", "owner": { "type": "user", "id": "1234567890123456789" } },
"created_at": "2026-07-07 16:00:00 UTC",
"updated_at": "2026-07-07 16:30:05 UTC",
"message_count": 3,
"continuable": true
},
"turns": {
"count": 1,
"items": [
{
"turn_id": "xYzAbCdEfGhIjKlMnO",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 1,
"status": "complete",
"idempotency_key": "3f2a…",
"query": { "text": "Summarize the quarterly report" },
"error": null,
"cost": { "credits": 5, "tokens": 500 },
"created_at": "2026-07-07 16:30:00 UTC",
"updated_at": "2026-07-07 16:30:05 UTC"
}
]
}
}
The turns.items entries are the lightweight turn shape (no answer blob). Fetch a single turn’s full answer, citations, and action replay via Get message details below. turns is seq-ascending (oldest first).
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Chat not found or not accessible |
1680 (Access Denied) | 401 | You do not have permission to access this thread |
Update a chat
/current/workspace/{workspace_id}/ai/agent/{chat_id}/update/
Update the name of an existing chat. The chat kind is set at creation and cannot be changed via this endpoint — any kind value supplied in the body is silently ignored.
Auth: Bearer token required. content_ai plan feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | New chat name |
Request example:
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/update/" \
-H "Authorization: Bearer {jwt_token}" \
-d "name=Updated Chat Name"
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat not found or locked |
1605 (Invalid Input) | 406 | Invalid name value |
1664 (Datastore Error) | 500 | Update failed |
Delete a chat
/current/workspace/{workspace_id}/ai/agent/{chat_id}/
Auth: Bearer token required. content_ai plan feature required.
Request example:
curl -X DELETE "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat not found or locked |
1654 (Internal Error) | 500 | Chat in non-deletable state or internal error |
Deleted chats can be listed via GET .../ai/agent/list/deleted.
Send a follow-up message
/current/workspace/{workspace_id}/ai/agent/{chat_id}/message/
Send a new message to an existing chat. The message is processed asynchronously.
Auth: Bearer token required. content_ai and ai_agent plan features required.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| question | string | Yes | — | Follow-up question, 1–32,000 characters. (May be omitted only when content_parts carries the message text.) |
| references | JSON array | No | — | File/folder reference items to attach as context — each a {type, id} file or folder item (see Attaching Files and Folders). Up to 20 files / 200 MB / 100 references; the backend resolves each item’s full details server-side. |
| content_parts | JSON array | No | — | Ordered content stream — text segments plus inline file/folder reference pills (same item shape as references). |
| subjects | JSON array | No | — | File/folder reference items pinned as focus subjects for the turn (same item shape as references). |
| uploads | JSON array | No | — | Focus files staged from outside the workspace/share. |
Also accepts the optional view, activity, role_in_org, and idempotency_key fields (see Advanced Per-Turn Fields above). There is no type parameter — the turn is appended to the existing thread.
Request example:
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/message/" \
-H "Authorization: Bearer {jwt_token}" \
-d "question=How does that compare to Q2?"
Response (200 OK):
{
"result": true,
"turn_id": "mNoPqRsTuVwXyZaBcD",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 2,
"status": "pending",
"idempotency_key": "7c1b…",
"query": { "text": "How does that compare to Q2?" },
"error": null,
"cost": { "credits": 0, "tokens": 0 },
"created_at": "2026-07-07 16:37:29 UTC",
"updated_at": "2026-07-07 16:37:29 UTC"
}
The created turn’s fields are returned at the top level (not nested under a message object). The turn_id is the message id you poll or stream. status starts at pending; watch it reach a terminal state (see “Get message details”).
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Thread not found, not accessible, or locked |
1680 (Access Denied) | 401 | You cannot message this thread; or (share chats only) folder attachment is not permitted for a restricted-view guest |
1609 (Not Found) | 404 | An attached file or folder reference does not exist or is not accessible |
1605 (Invalid Input) | 406 | An attached reference is malformed, the wrong node type, or exceeds the 20-file / 200 MB / 100-reference limit |
1660 (Conflict) | 409 | The conversation has grown too large to continue — start a new chat |
1664 (Datastore Error) | 500 | Transient storage error loading an attached file (retryable) |
1654 (Internal Error) | 500 | Message creation or queuing failed |
Cancel an in-progress message
/current/workspace/{workspace_id}/ai/agent/{chat_id}/cancel/
Aborts an in-flight AI message instead of waiting for it to finish or time out. The worker stops streaming and the turn transitions to a cancelled terminal state, after which a new message can be sent immediately.
Auth: Bearer token required. Workspace view permission. content_ai plan feature required (the cancel endpoint does not require ai_agent, so a tier downgrade mid-stream does not strand the message).
Body: Empty.
Request example:
curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/ai/agent/{chat_id}/cancel/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{ "result": true }
The response is always { "result": true } — there is no success, message.id, or no_pending_message field. It is the same whether a pending turn was signalled or there was nothing in flight (idempotent no-op).
Behavior notes:
- Scoped to the live turn. The signal targets the thread’s lowest-seq non-terminal turn. A thread with no in-flight turn is a clean success no-op.
- Idempotent. Calling cancel twice in quick succession returns
{ "result": true }both times. - Best-effort with bounded latency. The worker observes the cancel signal between streaming frames; observation typically happens within a few seconds. A turn that is between frames or already in post-processing may complete normally instead of cancelling.
- Partial billing. Tokens consumed up to the cancel point are charged (no refund), matching the behavior of streaming AI providers.
- SSE cancel signal. When the SSE read endpoint detects a cancellation it emits a single
event: cancelled(empty data) followed byevent: doneand then closes the stream. SSE clients should listen for the dedicatedcancelledevent (e.g.eventSource.addEventListener('cancelled', …)) rather than scanning data payloads — the read endpoint does not forward any raw[CANCELLED]text on the wire. - Turn status. After a successful cancel the affected turn reaches the
cancelledterminal state and a new message can be sent immediately. There is no thread-levelcancelledstatus.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat could not be loaded or the cancel signal could not be issued |
List messages in a chat
/current/workspace/{workspace_id}/ai/agent/{chat_id}/messages/list/
Returns all messages in chronological order (oldest first).
Auth: Bearer token required. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/messages/list/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"messages": {
"count": 1,
"items": [
{
"turn_id": "xYzAbCdEfGhIjKlMnO",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 1,
"status": "complete",
"idempotency_key": "3f2a…",
"query": { "text": "Summarize the quarterly report" },
"error": null,
"cost": { "credits": 5, "tokens": 500 },
"created_at": "2026-07-07 16:30:00 UTC",
"updated_at": "2026-07-07 16:30:05 UTC"
}
]
}
}
Each item is the lightweight turn shape (no answer blob) — see Message Object Schema. Fetch a turn’s full answer and citations via Get message details.
| Field | Type | Description |
|---|---|---|
messages | object | Collection envelope {count, items} |
messages.count | integer | Number of turn items returned in items |
messages.items | array | Array of turn objects, ordered oldest-first (seq ascending) |
A numeric path segment after /messages/list/ is a pagination offset (e.g. .../messages/list/50).
Get message details
/current/workspace/{workspace_id}/ai/agent/{chat_id}/message/{message_id}/details/
Retrieve detailed information about a specific message, including response text, citations, and cost.
Auth: Bearer token required. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/message/xYzAbCdEfGhIjKlMnO/details/" \
-H "Authorization: Bearer {jwt_token}"
The turn detail is returned under a message object on the workspace endpoint and a turn object on the share endpoint. It carries the full turn shape plus the decompressed answer result blob and the replayable actions list.
Key response fields (under message / turn):
| Field | Description |
|---|---|
turn_id | Opaque ID of this turn (message) |
thread_id | Parent thread (chat) opaque ID |
seq | Turn sequence number within the thread |
status | Turn status. One of pending, running, complete, failed, cancelled, lost, needs_input. (pending/running are non-terminal; the rest are terminal.) |
idempotency_key | The per-turn idempotency key |
query | The user’s submitted question: { text, content_parts?, references?, uploads?, subjects? } (internal server-only storage identifiers are stripped) |
error | { message, grpc_status } on a failed/lost turn; null otherwise |
cost | { credits, tokens } — the turn’s credit charge and raw token count |
created_at / updated_at | Timestamps (YYYY-MM-DD HH:MM:SS UTC) |
result | The decompressed answer blob (see below). null until the turn reaches a terminal state |
actions | Ordered, replayable action cards (see below). Empty when the turn took no actions |
Read the answer only when status is complete (or handle needs_input as a clarifying question — see the SSE needs_input event).
The result blob (when present) includes the answer text, references, citations (see Citation Format below), the thought_transcript / commentary_transcript strings and their ordered thought_events / commentary_events lists (each event { order, text, ts }), and — for a needs_input turn — a clarification object { type: "clarification", question }.
The actions list is ordered by seq — each entry has seq (order), label (human-readable name, e.g. "Create File"), state (running, done, failed, or cancelled), affected_refs (ids the action touched), and started_at / ended_at timestamps:
{
"message": {
"turn_id": "xYzAbCdEfGhIjKlMnO",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 1,
"status": "complete",
"query": { "text": "Summarize the quarterly report" },
"error": null,
"cost": { "credits": 5, "tokens": 500 },
"result": {
"answer": "The quarterly report shows revenue growth of 15%...",
"citations": [
{
"hash": "a1b2c3d4",
"nodeId": "f3jm5-zqzfx-pxdr2-dx8z5-bvnb3-rpjf",
"versionId": "v1abc-defgh-ijklm",
"entries": [
{ "page": 3, "snippet": "Revenue increased by 15% year over year...", "timestamp": null }
]
}
],
"thought_transcript": "",
"commentary_transcript": "",
"thought_events": [],
"commentary_events": []
},
"actions": [
{
"seq": 1,
"label": "Create File",
"state": "done",
"affected_refs": ["aBcDeFgHiJkLmNoPqR"],
"started_at": "2026-07-07 16:37:29 UTC",
"ended_at": "2026-07-07 16:37:30 UTC"
}
],
"created_at": "2026-07-07 16:37:00 UTC",
"updated_at": "2026-07-07 16:37:30 UTC"
}
}
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Chat not found or not accessible |
1683 (Resource Missing) | 404 | Message (turn) not found in the chat |
1680 (Access Denied) | 401 | You do not have permission to access this thread |
1654 (Internal Error) | 500 | Genuine internal/datastore failure |
Stream message response (SSE)
/current/workspace/{workspace_id}/ai/agent/{chat_id}/message/{message_id}/read/
Returns a Server-Sent Events (SSE) stream of the AI response.
Auth: Bearer token required. content_ai plan feature required.
Request example:
curl -N -X GET "https://api.fast.io/current/workspace/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/message/xYzAbCdEfGhIjKlMnO/read/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Accept: text/event-stream"
SSE stream format:
event: data
data: {"item": "The quarterly report "}
event: data
data: {"item": "shows revenue growth of "}
event: data
data: {"item": "15% year over year."}
event: analysis_data
data: {"type": "analysis_chunk", ...}
event: table_data
data: {"type": "table_data", ...}
event: done
SSE event types:
| Event Type | Description |
|---|---|
data | Text chunks of the AI response. Payload: {"item": "..."}. Concatenate all data events for the full text. |
event | Status update or event notification |
analysis_data | Structured analysis data, citations, and references to source files |
commentary | Interim narration the AI emits while working. Payload: {"text": "...", "parts": [...]} — text is the flattened narration, parts the ordered content parts (text and inline object references). Expect one per work step on multi-step responses. |
status | Cosmetic turn-progress hint emitted before the agent's first output frame. Payload: {"phase": "...", "text": "..."} — phase is enhancing (first turn only, while the question is enhanced/evaluated; clients commonly show "Analyzing your request…") or invoking_agent (every turn, just before the agent runs; clients commonly show "Connecting agent…"), text a human-readable default you may show or override. Not persisted and not replayed from the durable record — treat as a best-effort indicator and clear it once real output (or a terminal event) arrives. Unknown phases → generic "working". |
table_data | Tabular data extracted or generated by the AI |
needs_input | Terminal event: the assistant needs more information and returned a single clarifying question instead of a full response. The question text arrives on a preceding data frame (payload includes a question field); fetch the message details to read it from the result's clarification object. The message reaches a needs_input terminal state (not failed) — present the question and send the user's answer as a new message in the same chat. Listen for it as its own event; the stream closes after it. |
done | Stream complete. No more events will be sent. |
Behavior:
- For a terminal turn (
complete,failed,cancelled,lost,needs_input), the stored events are replayed immediately, followed by the matching terminal event, then the stream closes. - For a running (or freshly
pending) turn, the connection stays open and live-follows, emitting frames as the worker produces them. A single best-effortstatusframe is sent on connect so the client leaves its “connecting” state immediately. - Resume is supported. Pass a
Last-Event-IDheader to resume after the last event you received; when the live stream has expired the events are synthesized from the canonical record. - The connection auto-terminates after a bounded wait while a turn is still running (browsers auto-reconnect via
EventSourceand resume withLast-Event-ID). - Headers include
Cache-Control: no-cache, no-store, must-revalidate. - This endpoint returns an SSE stream, NOT the standard JSON response envelope.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1683 (Resource Missing) | 404 | Message (turn) not found in the chat |
1609 (Not Found) | 404 | Chat not found or not accessible |
1680 (Access Denied) | 401 | You do not have permission to read this thread |
1654 (Internal Error) | 500 | Genuine internal/datastore failure |
Publish a private chat
/current/workspace/{workspace_id}/ai/agent/{chat_id}/publish/
Makes a private chat public (visible to other workspace members). One-way operation — published chats cannot be made private again.
Currently disabled (platform-wide). Publishing a chat publicly is turned off for all accounts: this endpoint returns 403 Forbidden with message "Publishing chats publicly is currently disabled.", and creating a chat with privacy=public is refused the same way. Clients can detect availability via the capabilities.can_publish_agent_chat boolean on workspace details (currently false) and hide the publish control. Chats already published before this change remain public.
Auth: Bearer token required. content_ai plan feature required.
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat not found or locked |
1660 (Conflict) | 409 | Chat is already public |
1664 (Datastore Error) | 500 | Update failed |
Generate AI Share
/current/workspace/{workspace_id}/ai/share/
Generates markdown with temporary download URLs for selected files. Designed to be pasted into external AI chatbots.
Auth: Bearer token required. Workspace view permission. Does NOT require content_ai plan feature — available on all plans.
| Parameter | Type | Required | Description |
|---|---|---|---|
| files | array (JSON) | Yes | JSON array of file opaque IDs. Min 1, max 25. |
Request example:
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/ai/share/" \
-H "Authorization: Bearer {jwt_token}" \
--data-urlencode 'files=["aBcDeFgHiJkLmN", "oPqRsTuVwXyZ12"]'
The endpoint reads form-encoded input (application/x-www-form-urlencoded). The files field value must be a JSON-encoded array of node opaque IDs. Do NOT send a JSON request body (Content-Type: application/json) — only form-encoded bodies are parsed.
Response (200 OK):
{
"result": true,
"markdown": "## Files\n\n### quarterly-report.pdf\n[Download](https://api.fast.io/...)\nSize: 2.5 MB\n\n..."
}
| Field | Type | Description |
|---|---|---|
response.markdown | string | Generated markdown with file info and temporary download URLs |
Notes:
- Download URLs expire after 5 minutes (300 seconds)
- Each token can be used a maximum of 3 times
- Individual files limited to 50 MB; total size limited to 100 MB
- When more than 5 files: titles only. 5 or fewer: includes full descriptions.
- The
filesinput is a JSON array (not comma-separated strings)
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Empty files array |
1605 (Invalid Input) | 406 | More than 25 files |
List AI transactions
/current/workspace/{workspace_id}/ai/transactions/
Returns up to 40 most recent AI token usage transactions for the workspace. Workspace-only — no share equivalent.
Results merge two sources into one most-recent-first feed: standalone AI operations (file summaries, title generation, indexing, and other one-off AI tasks) and completed agent conversation turns. Agent-turn entries carry type agent; standalone operations carry their operation type (e.g. chat_with_files, generate_title). In-progress turns are not included — only finished work appears.
Auth: Bearer token required. Workspace view permission. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/ai/transactions/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"count": 2,
"items": [
{
"id": "txn_abc123",
"type": "chat_with_files",
"status": "complete",
"tokens": 1500,
"updated": "2025-06-15 10:30:05 UTC",
"created": "2025-06-15 10:30:00 UTC"
}
]
}
| Field | Type | Description |
|---|---|---|
response.count | integer | Number of transactions returned |
response.items[].id | string | Formatted transaction or turn ID |
response.items[].type | string | Operation type (e.g., chat_with_files, generate_title) or agent for a completed agent conversation turn |
response.items[].status | string | Transaction or turn status (e.g., complete, failed, cancelled, needs_input). needs_input marks a turn the assistant answered with a clarifying question instead of a full response. |
response.items[].tokens | integer | Token credits consumed |
response.items[].updated | string | Last update timestamp (YYYY-MM-DD HH:MM:SS UTC) |
response.items[].created | string | Creation timestamp (YYYY-MM-DD HH:MM:SS UTC) |
Asking a Question and Getting the Response
Complete workflow
1. Create the chat:
curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/ai/agent/" \
-H "Authorization: Bearer {jwt_token}" \
-d "question=What were the Q3 revenue figures?"
The response includes thread.thread_id (the chat id) and turn.turn_id (the first message id). The AI begins processing asynchronously.
2. Wait for completion using activity polling (do NOT poll the message endpoint in a loop):
curl -X GET "https://api.fast.io/current/activity/poll/{workspace_id}?wait=95&lastactivity={timestamp}" \
-H "Authorization: Bearer {jwt_token}"
Watch for the ai_chat:{chatId} activity key. This fires when the message state changes. The server holds the connection for up to 95 seconds and returns immediately when something changes.
3. Check message state:
curl -X GET "https://api.fast.io/current/workspace/{workspace_id}/ai/agent/{chat_id}/message/{message_id}/details/" \
-H "Authorization: Bearer {jwt_token}"
Turn states: pending → running → complete (or the terminal failed, cancelled, lost, or needs_input). Only read the answer when the status is complete (handle needs_input as a clarifying question).
4. Stream the response:
curl -N -X GET "https://api.fast.io/current/workspace/{workspace_id}/ai/agent/{chat_id}/message/{message_id}/read/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Accept: text/event-stream"
Returns SSE with event types: data (text chunks), commentary (interim narration), status (turn-progress hints, before the agent’s first frame), analysis_data, table_data, needs_input (terminal: a single clarifying question instead of a full answer — present it and send the user’s reply as a new message), done.
5. Send follow-ups:
curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/ai/agent/{chat_id}/message/" \
-H "Authorization: Bearer {jwt_token}" \
-d "question=How does that compare to Q2?"
Same polling flow for the reply.
Linking a user to an AI chat
Construct a workspace URL with a chat query parameter:
https://{org_domain}.fast.io/workspace/{workspace_name}?chat={chat_opaque_id}
The chat_opaque_id is the thread’s opaque id — returned as thread.thread_id when creating a chat, and as each item’s thread_id when listing chats.
Share AI Endpoints
Share AI endpoints follow the same pattern as workspace AI. Replace /workspace/{workspace_id} with /share/{share_id}.
Auto-generate OG image
/current/share/{share_id}/ai/autoog/
Generates an Open Graph image for the share. Returns binary PNG image data (not JSON).
Auth: Conditional. Public shares (Anyone / RegisteredUsers) need no authentication and get a custom AI-generated image. Private shares require Bearer auth and the ai_autoog plan feature; without valid permission or the feature, the endpoint falls back to the default private OG image (still HTTP 200 image data).
| Behavior | Description |
|---|---|
| Public share | Custom AI-generated image based on share content |
| Private share | Custom image when the caller has permission and the ai_autoog feature; otherwise the default private image |
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Share is disabled |
1654 (Internal Error) | 500 | Default image not found on server |
Auto-generate title and description
/current/share/{share_id}/ai/autotitle/
AI-generates a title, description, and display type based on the share’s contents. Values are applied directly to the share.
Auth: Bearer token required. Requires the ai_autotitle plan feature (not content_ai/ai_agent) — a plan without it is rejected.
| Parameter | Type | Required | Description |
|---|---|---|---|
| user_context | string | No | Optional user-provided context to guide AI generation |
Request example:
curl -X POST "https://api.fast.io/current/share/1234567890123456789/ai/autotitle/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"title": "Q4 Financial Reports",
"description": "Quarterly financial reports and analysis for fiscal year 2025.",
"display_type": "document"
}
| Field | Type | Description |
|---|---|---|
response.title | string | AI-generated title |
response.description | string | AI-generated description |
response.display_type | string | AI-suggested display type |
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1680 (Access Denied) | 401 | Insufficient share permissions |
1654 (Internal Error) | 500 | Share update or generation failure |
Share chat endpoints mirror workspace chat endpoints. All response formats and schemas are identical to the workspace versions documented above. The key differences are:
- Auth uses share permissions instead of workspace permissions
- File scope references share files instead of workspace files
- No AI Transactions endpoint (workspace-only)
Create a new chat (Share)
/current/share/{share_id}/ai/agent/
Creates a chat with an initial message in a share. The AI begins processing asynchronously.
Auth: Bearer token required. Share view permission and chat permission. content_ai and ai_agent plan features required.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| question | string | Yes | — | Initial question, 1–32,000 characters. (May be omitted only when content_parts carries the message text.) |
| name | string | No | Auto-generated | Chat name. A default is used if omitted. |
| kind | string | No | user | Share-context creation only produces user chats. |
| references | JSON array | No | — | File/folder reference items to attach as context — each a {type, id} file or folder item (see Attaching Files and Folders). Up to 20 files / 200 MB / 100 references; the backend resolves each item’s full details server-side. |
| content_parts | JSON array | No | — | Ordered content stream — text segments plus inline file/folder reference pills (same item shape as references). |
| subjects | JSON array | No | — | File/folder reference items pinned as focus subjects for the turn (same item shape as references). |
| uploads | JSON array | No | — | Focus files staged from outside the share. |
Share-context chats are always private — the privacy parameter is not accepted and visibility is fixed so guests do not see each other's AI conversations. Also accepts the optional view, activity, role_in_org, and idempotency_key fields.
Request example:
curl -X POST "https://api.fast.io/current/share/1234567890123456789/ai/agent/" \
-H "Authorization: Bearer {jwt_token}" \
-d "question=What were the Q3 revenue figures?"
Response (200 OK):
Same { thread, turn } shape as the workspace create endpoint. thread.thread_id is the chat id; turn.turn_id is the first message id. See Create a new chat (workspace) above for the full field lists.
{
"result": true,
"thread": { "thread_id": "aBcDeFgHiJkLmNoPqR", "scope": { "type": "share", "id": "1234567890123456789" }, "status": "active", "kind": "user", "...": "..." },
"turn": { "turn_id": "xYzAbCdEfGhIjKlMnO", "thread_id": "aBcDeFgHiJkLmNoPqR", "seq": 1, "status": "pending", "...": "..." }
}
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Invalid kind or name, or invalid question length |
1609 (Not Found) | 404 | An attached file or folder reference does not exist or is not accessible |
1605 (Invalid Input) | 406 | An attached reference is malformed, the wrong node type, or exceeds the 20-file / 200 MB / 100-reference limit |
1680 (Access Denied) | 401 | Folder attachment is not permitted in this share (restricted-view guest) |
1660 (Conflict) | 409 | Thread still committing its first turn (retry), or the first message is too large |
1664 (Datastore Error) | 500 | Thread or turn creation failed |
List chats (Share)
/current/share/{share_id}/ai/agent/list/
Returns all chats created by the current user in the share. Sorted by most recently modified first.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Query parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| kind | string | No | user | Filter by chat kind. Allowed values: user (default), agent, all. Same semantics as the workspace list endpoint. Note: share-context chat creation does not accept kind, so all share-created chats are user. |
Variant: Append /deleted to the path to list deleted chats: GET .../ai/agent/list/deleted
Request example:
curl -X GET "https://api.fast.io/current/share/1234567890123456789/ai/agent/list/" \
-H "Authorization: Bearer {jwt_token}"
Response: Same format as workspace chat list. See List chats above.
Get chat details (Share)
/current/share/{share_id}/ai/agent/{chat_id}/details/
Returns chat details with full message history.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/details/" \
-H "Authorization: Bearer {jwt_token}"
Response: Same format as workspace chat details. See Get chat details above.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Chat not found or not accessible |
1680 (Access Denied) | 401 | You do not have permission to access this thread |
Update a chat (Share)
/current/share/{share_id}/ai/agent/{chat_id}/update/
Update the name of an existing chat in a share.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | New chat name |
Request example:
curl -X POST "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/update/" \
-H "Authorization: Bearer {jwt_token}" \
-d "name=Updated Chat Name"
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat not found or locked |
1605 (Invalid Input) | 406 | Invalid name value |
1664 (Datastore Error) | 500 | Update failed |
Delete a chat (Share)
/current/share/{share_id}/ai/agent/{chat_id}/
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Request example:
curl -X DELETE "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat not found or locked |
1654 (Internal Error) | 500 | Chat in non-deletable state or internal error |
Deleted chats can be listed via GET .../ai/agent/list/deleted.
Send a follow-up message (Share)
/current/share/{share_id}/ai/agent/{chat_id}/message/
Send a new message to an existing chat in a share. The message is processed asynchronously.
Auth: Bearer token required. Share view permission and chat permission. content_ai and ai_agent plan features required.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| question | string | Yes | — | Follow-up question, 1–32,000 characters. (May be omitted only when content_parts carries the message text.) |
| references | JSON array | No | — | File/folder reference items to attach as context — each a {type, id} file or folder item (see Attaching Files and Folders). Up to 20 files / 200 MB / 100 references; the backend resolves each item’s full details server-side. |
| content_parts | JSON array | No | — | Ordered content stream — text segments plus inline file/folder reference pills (same item shape as references). |
| subjects | JSON array | No | — | File/folder reference items pinned as focus subjects for the turn (same item shape as references). |
| uploads | JSON array | No | — | Focus files staged from outside the workspace/share. |
Also accepts the optional view, activity, role_in_org, and idempotency_key fields (see Advanced Per-Turn Fields above). There is no type parameter — the turn is appended to the existing thread.
Request example:
curl -X POST "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/message/" \
-H "Authorization: Bearer {jwt_token}" \
-d "question=How does that compare to Q2?"
Response (200 OK):
{
"result": true,
"turn_id": "mNoPqRsTuVwXyZaBcD",
"thread_id": "aBcDeFgHiJkLmNoPqR",
"seq": 2,
"status": "pending",
"idempotency_key": "7c1b…",
"query": { "text": "How does that compare to Q2?" },
"error": null,
"cost": { "credits": 0, "tokens": 0 },
"created_at": "2026-07-07 16:37:29 UTC",
"updated_at": "2026-07-07 16:37:29 UTC"
}
The created turn’s fields are returned at the top level (not nested under a message object). The turn_id is the message id you poll or stream. status starts at pending; watch it reach a terminal state (see “Get message details”).
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Thread not found, not accessible, or locked |
1680 (Access Denied) | 401 | You cannot message this thread; or (share chats only) folder attachment is not permitted for a restricted-view guest |
1609 (Not Found) | 404 | An attached file or folder reference does not exist or is not accessible |
1605 (Invalid Input) | 406 | An attached reference is malformed, the wrong node type, or exceeds the 20-file / 200 MB / 100-reference limit |
1660 (Conflict) | 409 | The conversation has grown too large to continue — start a new chat |
1664 (Datastore Error) | 500 | Transient storage error loading an attached file (retryable) |
1654 (Internal Error) | 500 | Message creation or queuing failed |
Cancel an in-progress message (Share)
/current/share/{share_id}/ai/agent/{chat_id}/cancel/
Aborts an in-flight AI message in a share chat. Behavior matches the workspace cancel endpoint above: the worker halts streaming and the affected turn reaches a cancelled terminal state, after which a new message can be sent.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required (the cancel endpoint does not require ai_agent).
Body: Empty.
Request example:
curl -X POST "https://api.fast.io/current/share/{share_id}/ai/agent/{chat_id}/cancel/" \
-H "Authorization: Bearer {jwt_token}"
Response: { "result": true } — same as the workspace cancel endpoint (no success, message.id, or no_pending_message field). See Cancel an in-progress message above for the full behavior notes (idempotency, best-effort latency, partial billing, SSE cancelled event, and the affected turn reaching the cancelled terminal state).
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat could not be loaded or the cancel signal could not be issued |
List messages in a chat (Share)
/current/share/{share_id}/ai/agent/{chat_id}/messages/list/
Returns all messages in chronological order (oldest first).
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/messages/list/" \
-H "Authorization: Bearer {jwt_token}"
Response: Same format as workspace message list. See List messages in a chat above.
Get message details (Share)
/current/share/{share_id}/ai/agent/{chat_id}/message/{message_id}/details/
Retrieve detailed information about a specific message, including response text, citations, and cost.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Request example:
curl -X GET "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/message/xYzAbCdEfGhIjKlMnO/details/" \
-H "Authorization: Bearer {jwt_token}"
Response: Same format as workspace message details. See Get message details above.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Chat not found or not accessible |
1683 (Resource Missing) | 404 | Message (turn) not found in the chat |
1680 (Access Denied) | 401 | You do not have permission to access this thread |
1654 (Internal Error) | 500 | Genuine internal/datastore failure |
Stream message response (SSE) (Share)
/current/share/{share_id}/ai/agent/{chat_id}/message/{message_id}/read/
Returns a Server-Sent Events (SSE) stream of the AI response.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Request example:
curl -N -X GET "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/message/xYzAbCdEfGhIjKlMnO/read/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Accept: text/event-stream"
SSE stream format and behavior: Identical to the workspace version. See Stream message response (SSE) above.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1683 (Resource Missing) | 404 | Message (turn) not found in the chat |
1609 (Not Found) | 404 | Chat not found or not accessible |
1680 (Access Denied) | 401 | You do not have permission to read this thread |
1654 (Internal Error) | 500 | Genuine internal/datastore failure |
Publish a private chat (Share)
/current/share/{share_id}/ai/agent/{chat_id}/publish/
Makes a private chat public (visible to other share members). One-way operation — published chats cannot be made private again.
Currently disabled (platform-wide). Publishing a chat publicly is turned off for all accounts: this endpoint returns 403 Forbidden with message "Publishing chats publicly is currently disabled." Clients can detect availability via the capabilities.can_publish_agent_chat boolean on share details (currently false) and hide the publish control. Chats already published before this change remain public.
Auth: Bearer token required. Share view permission and chat permission. content_ai plan feature required.
Request example:
curl -X POST "https://api.fast.io/current/share/1234567890123456789/ai/agent/aBcDeFgHiJkLmNoPqR/publish/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1658 (Not Acceptable) | 406 | Chat not found or locked |
1660 (Conflict) | 409 | Chat is already public |
1664 (Datastore Error) | 500 | Update failed |
Generate AI Share (Share)
/current/share/{share_id}/ai/share/
Generates markdown with temporary download URLs for selected files. Designed to be pasted into external AI chatbots.
Auth: Bearer token required. Share view permission and download permission. Does NOT require content_ai plan feature — available on all plans.
| Parameter | Type | Required | Description |
|---|---|---|---|
| files | array (JSON) | Yes | JSON array of file opaque IDs. Min 1, max 25. |
Request example:
curl -X POST "https://api.fast.io/current/share/1234567890123456789/ai/share/" \
-H "Authorization: Bearer {jwt_token}" \
--data-urlencode 'files=["aBcDeFgHiJkLmN", "oPqRsTuVwXyZ12"]'
The endpoint reads form-encoded input (application/x-www-form-urlencoded). The files field value must be a JSON-encoded array of node opaque IDs. Do NOT send a JSON request body (Content-Type: application/json) — only form-encoded bodies are parsed.
Response (200 OK):
{
"result": true,
"markdown": "## Files\n\n### quarterly-report.pdf\n[Download](https://api.fast.io/...)\nSize: 2.5 MB\n\n..."
}
| Field | Type | Description |
|---|---|---|
response.markdown | string | Generated markdown with file info and temporary download URLs |
Notes:
- Download URLs expire after 5 minutes (300 seconds)
- Each token can be used a maximum of 3 times
- Individual files limited to 50 MB; total size limited to 100 MB
- When more than 5 files: titles only. 5 or fewer: includes full descriptions.
- The
filesinput is a JSON array (not comma-separated strings)
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Empty files array |
1605 (Invalid Input) | 406 | More than 25 files |
1680 (Access Denied) | 401 | Insufficient download permissions |
Workspace AI vs. Share AI differences
| Feature | Workspace AI | Share AI |
|---|---|---|
| AI Transactions endpoint | Yes | No |
| Auto OG image endpoint | No | Yes |
| Auto title endpoint | No | Yes |
content_ai feature required | Yes (except AI Share) | Yes (except AI Share) |
ai_agent feature required | Yes, for create-chat and send-message (except AI Share) | Yes, for create-chat and send-message (except AI Share) |
| File scope context | Workspace files | Share files |
AI Share File Download
/current/ai/share/{token}?file={index}
Download a file from an AI Share using a temporary token. No authentication required — access is controlled by the token.
| Parameter | Type | Required | Description |
|---|---|---|---|
| {token} | string (path) | Yes | Alphanumeric AI share token (generated by the AI Share creation endpoint) |
| file | integer (query) | Yes | Zero-based file index within the AI Share |
Request example:
curl -X GET "https://api.fast.io/current/ai/share/aBcDeFgHiJkLmNoPqRsTuVwXyZ?file=0" \
-o downloaded_file.pdf
Success response: Binary file data with appropriate Content-Type, Content-Disposition, Content-Length, and Accept-Ranges headers. Supports HTTP range requests.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Token missing, invalid, expired, or use limit reached |
1609 (Not Found) | 404 | File index out of range |
1654 (Internal Error) | 500 | Unable to retrieve or read file |
All invalid/expired token errors return 404 to prevent token enumeration.
Semantic Search
/ai/search/ endpoint is deprecated. Use GET /current/workspace/{id}/storage/search/ (or /share/{id}/storage/search/) instead. The unified storage search endpoint automatically performs semantic search when workspace intelligence is enabled. Pass the search parameter for your query, and optionally files_scope and folders_scope to narrow results. Semantic results include relevance_score, content_snippet, match_source, media_segment, mimetype, and search_metadata fields.
The legacy endpoints below continue to work but will be removed in a future release.
Workspace Semantic Search (Deprecated)
/current/workspace/{workspace_id}/ai/search/
Auth: Bearer token required. Workspace view permission. content_ai plan feature required.
Share Semantic Search (Deprecated)
/current/share/{share_id}/ai/search/
Identical to workspace semantic search but scoped to a share.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| question | string | Yes | — | Search query, 2–1,000 characters. (The parameter is named question, not query_text.) |
| files_scope | string | No | All indexed files | Comma-separated nodeId:versionId pairs (max 100) |
| folders_scope | string | No | All indexed files | Comma-separated nodeId:depth pairs (max 100, depth 1–10) |
| limit | integer | No | 20 | Results per page, 1–500 |
| offset | integer | No | 0 | Pagination offset |
This deprecated endpoint has no details parameter — node enrichment via details=true is a feature of /storage/search (below), not this endpoint. It also returns 1605 (Invalid Input) / 406 when workspace intelligence is not enabled.
Preferred approach — use /storage/search instead:
# Basic search
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/storage/search/?search=quarterly%20revenue&limit=10" \
-H "Authorization: Bearer {jwt_token}"
# Search with full node details
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/storage/search/?search=quarterly%20revenue&details=true" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"response_code": 200,
"results": [
{
"content": "The quarterly revenue showed a 15% increase...",
"score": 0.95,
"node": {
"id": "f3jm5-zqzfx-pxdr2-dx8z5-bvnb3-rpjf",
"type": "file",
"name": "quarterly-report.pdf",
"parent": "root",
"size": 1048576,
"mimetype": "application/pdf",
"ai": { "state": "indexed", "attach": true, "summary": true }
}
}
],
"pagination": {
"total": 25,
"limit": 10,
"offset": 0,
"has_more": true
}
}
| Field | Type | Description |
|---|---|---|
results[].content | string | Matched text snippet from the indexed document |
results[].score | float | Relevance score (0.0–1.0, higher is more relevant) |
results[].node | object/null | Full node resource, or null if the file was deleted |
results[].file_details | object | Raw metadata when node is null (node_id, version_id, name, mimetype) |
pagination.total | integer | Total number of results |
pagination.has_more | boolean | Whether more results are available |
Hybrid response fields (intelligence enabled):
When workspace intelligence is enabled, each file entry in the /storage/search response includes additional semantic fields:
| Field | Type | Description |
|---|---|---|
relevance_score | float | Semantic relevance score (0.0–1.0) |
content_snippet | string/null | The actual matching text from semantic search. NULL for keyword-only matches. Trimmed by the output query param on /storage/search/ (terse ~200 bytes, standard ~600 bytes, full untrimmed; truncated values end with …). |
match_source | string | Source of the match: keyword, semantic, or both |
mimetype | string | File MIME type (e.g., application/pdf, audio/mpeg). Present for semantic matches. |
media_segment | object | Only for audio/video matches when intelligence is on. Contains start_seconds and end_seconds for deep-linking to the exact timestamp range. |
search_metadata | object | Additional search metadata |
Response with details=true (via /storage/search):
When using the preferred /storage/search endpoint with ?details=true, each file entry includes a node field with the full node resource:
{
"result": true,
"files": {
"f3jm5-zqzfx-pxdr2-dx8z5-bvnb3-rpjf": {
"name": "report.pdf",
"parent_id": "...",
"type": "file",
"relevance_score": 0.92,
"content_snippet": "Revenue increased 15%...",
"match_source": "both",
"mimetype": "application/pdf",
"node": {
"id": "f3jm5-zqzfx-pxdr2-dx8z5-bvnb3-rpjf",
"name": "report.pdf",
"type": "file",
"size": 123456,
"previews": { "...": "..." },
"ai": { "state": "indexed" }
}
}
}
}
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Intelligence not enabled |
1605 (Invalid Input) | 406 | Malformed files_scope or folders_scope |
How to Phrase Questions
With folder/file scope (RAG)
Write questions that will match content in indexed files. The AI searches for relevant passages and cites them. Be specific.
- Good: “What were the revenue figures for Q3 2025 compared to Q2?”
- Good: “Summarize the key findings from the compliance audit reports”
- Bad: “Tell me about these files” — too vague, no searchable content to match
With file attachments
You can be more direct since the AI has the full file content.
- Good: “Describe this image in detail” (with an image attached)
- Good: “Extract all action items from this meeting transcript”
- Good: “Compare these two contracts and list the differences”
Chat Session Object Schema
The thread (chat) resource is returned as thread by the details endpoint and as each chats.items[] entry by the list endpoint. The details endpoint returns the message history separately as a turns collection — there is no embedded messages array on the thread.
| Field | Type | Description |
|---|---|---|
thread_id | string | Opaque ID of the thread (the chat) |
creator | object | {type: string, id: string} — the chat creator |
scope | object | {type: string, id: string} — the workspace or share the chat lives in |
name | string | Display name of the chat |
status | string | Current chat status |
kind | string | user or agent — set at creation, immutable thereafter |
cost | object | {credits: int, tokens: int} — credit charge and the raw token count it derives from |
privacy | object | {visibility: "private"|"public", owner: {type, id}|null} |
created_at | string | Creation timestamp (YYYY-MM-DD HH:MM:SS UTC) |
updated_at | string | Last update timestamp (YYYY-MM-DD HH:MM:SS UTC) |
message_count | integer | Total turns (read endpoints only; omitted on mutation acks) |
continuable | boolean | Whether the chat can be continued (read endpoints only) |
latest_message | object/null | Most recent turn preview (list endpoint only) |
There is no type, unique_creators, or efficiency field.
Message Object Schema
A message is a turn. The lightweight shape (list/details/mutation responses) carries the fields above the divider; the per-turn detail endpoint additionally returns result and actions.
| Field | Type | Description |
|---|---|---|
turn_id | string | Opaque ID of the turn (the message) |
thread_id | string | Parent thread (chat) opaque ID |
seq | integer | Turn sequence number within the thread |
status | string | pending, running, complete, failed, cancelled, lost, or needs_input |
idempotency_key | string | Per-turn idempotency key |
query | object | The user’s submitted question: { text, content_parts?, references?, uploads?, subjects? } |
error | object/null | { message: string, grpc_status: int|null } on a failed/lost turn; null otherwise |
cost | object | { credits: int, tokens: int } — the turn’s credit charge and raw token count |
created_at | string | Creation timestamp (YYYY-MM-DD HH:MM:SS UTC) |
updated_at | string | Last update timestamp (YYYY-MM-DD HH:MM:SS UTC) |
result | object/null | Detail view only. Decompressed answer blob: answer, references, citations, thought_transcript, commentary_transcript, thought_events, commentary_events, and (for needs_input) clarification. null until the turn is terminal |
actions | array | Detail view only. Ordered, replayable action cards (seq, label, state, affected_refs, started_at, ended_at) |
There is no state, personality, response, author_name, or top-level text/citations/events field — the answer and its citations live inside the detail view’s result blob, and the processing state is status.
Citation Format
Citations appear inside a completed turn’s result blob (under result.citations / result.references), fetched from Get message details. They reference specific locations in files that informed the AI response.
| Field | Type | Description |
|---|---|---|
hash | string | File content hash (used for grouping) |
nodeId | string | Storage node opaque ID |
versionId | string | File version opaque ID |
entries | array | Citation locations within the file |
entries[].page | integer | Page number in the document |
entries[].snippet | string/null | Relevant text excerpt |
entries[].timestamp | float/null | Timestamp for audio/video files (seconds) |
Activity Polling for AI Chat Completion
Do NOT poll the message details endpoint in a loop. Use activity long-polling instead.
/current/activity/poll/{workspace_id}?wait=95&lastactivity={timestamp}
The server holds the connection for up to 95 seconds and returns immediately when something changes. Watch for the ai_chat:{chatId} activity key — this fires when the message state changes.
| Activity Key Pattern | What Changed |
|---|---|
ai_chat:{chatId} | AI chat message state updated |
storage:{fileId} | File added, updated, or removed |
preview:{fileId} | File preview/thumbnail is ready |
Pass the returned lastactivity timestamp into your next poll to receive only newer changes.
Anti-pattern: Do not GET .../ai/agent/{id}/message/{id}/details/ in a loop. Poll once on the workspace activity endpoint and wait for the ai_chat key.
Metadata Templates
Structured metadata for workspace files. Templates define schemas (fields, types, constraints) and have many-to-many relationships with files — a template can be applied to multiple files, and files can have metadata from multiple templates. Templates are managed at the workspace level.
The metadata feature is available on all plan tiers, with three distinct caps that scale by plan:
| Plan | Max Templates | Max Files per Template | Max Fields (Columns) per Template |
|---|---|---|---|
| Starter | 2 | 1000 | 10 |
| Business | 10 | 1000 | 50 |
| Growth | 10 | 1000 | 50 |
Listing, details, and preview endpoints return plan_node_limit, is_truncated, and an unfiltered count alongside the visible (capped) count, so consumers can render upsell messaging. Manual /nodes/add rejects requests that would exceed the per-template file cap. Auto-match silently truncates its candidate set to the remaining slots and incurs no LLM cost when the template is already at its cap. On downgrade, overflow rows are preserved but hidden; under truncation, the visible window is ordered by mapping creation order (oldest-mapped first), so the same files remain visible across plan changes and re-upgrade restores full visibility.
The field cap bounds the number of fields (columns) declared in a template's schema. It is enforced at every commit point that mutates the field set: create, update, and copy. Exceeding the cap returns 1605 (Invalid Input) with a message describing the attempted field count and the cap. The /details and /list endpoints surface the current declared field count as field_count and the per-plan ceiling as plan_field_limit (-1 when unlimited or unresolved).
Each template field also carries an autoextract boolean (defaults to true). Fields with autoextract=false are excluded from the default auto-extraction scope, which is useful for purely manual fields. Every template must have at least one field with autoextract=true; templates that opt every field out are rejected at create/update/copy. A full-row extract request that would yield an empty effective scope short-circuits with success and does not enqueue a job; an explicit fields array on the extract endpoint overrides the flag and is always honored.
Per-node autoextract eligibility is surfaced as an autoextractable boolean on both the metadata details endpoint (GET /workspace/{id}/storage/{node_id}/metadata/details/) and the template nodes listing (GET /workspace/{id}/metadata/templates/{tid}/nodes). It is true when the node is a non-trashed file with a completed AI summary — the same signal the extraction pipeline uses — and clients can use it to gate "extract now" affordances without making an extra probe.
Files are linked to templates either manually (add/remove endpoints) or automatically via AI-based matching. When intelligence is enabled, metadata is automatically extracted during file ingestion for documents, spreadsheets, images (PNG, JPEG, WebP), and code files.
Compact Responses (output=)
Every metadata endpoint that returns object-metadata, template, saved-view, or eligible-node records accepts an optional output query parameter that selects the shape of each record in the response. A single detail-level token may be combined with modifier tokens; specifying two detail levels (e.g. ?output=terse,standard) returns HTTP 406. When output= is omitted, responses are full and byte-for-byte unchanged.
Object metadata (GET /workspace/{id}/storage/{node}/metadata/details/):
| Level | Fields returned on each metadata record (cumulative) |
|---|---|
terse | object_id, template_id, node_id (narrowed to {id, name, type}), metadata (the full key/value payload) |
standard | terse + instance_id, node_id widened to {id, name, type, parent, mimetype}, autoextractable |
full | everything |
The key/value payload is the point of a metadata response, so terse keeps it. The savings come from trimming the nested node pointer — terse carries only the minimum identity fields needed to address the node in a follow-up call, standard adds parent and mimetype for list rendering, and full keeps the entire node resource.
Templates (GET /workspace/{id}/metadata/templates/list/, GET /workspace/{id}/metadata/templates/{tid}/details/):
| Level | Fields returned (cumulative) |
|---|---|
terse | id, name, description, enabled, priority |
standard | terse + instanceId, orgId, locked, deleted, updated, created, fields, node_count, field_count, node_count_capped, plan_node_limit, is_truncated, plan_field_limit |
full | everything |
deleted is included in standard so trash/restore workflows can surface the soft-delete timestamp without promoting to full.
Saved views (GET /workspace/{id}/metadata/views/):
| Level | Fields returned (cumulative) |
|---|---|
terse | name |
standard | terse + node_id, filters, order_by, order_desc, created, updated |
full | everything |
Eligible nodes (GET /workspace/{id}/metadata/eligible/):
| Level | Fields returned (cumulative) |
|---|---|
terse | node_id, name, mimetype |
standard | terse + size, summary_title, summary_short, updated, templates |
full | everything |
Unknown tokens are silently ignored. Add the markdown modifier (e.g. ?output=standard,markdown) to receive the response as GitHub-flavored Markdown (Content-Type: text/markdown; charset=UTF-8) instead of JSON — see the cross-cutting ?output= reference for the full contract.
Create a template
/current/workspace/{workspace_id}/metadata/templates/
Create a new metadata template defining fields, types, and constraints.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Template name (1–100 characters) |
| description | string | Yes | Template description (max 255 characters) |
| fields | string (JSON) | Yes | JSON-encoded array of field definitions |
There is no category parameter — the create endpoint accepts only name, description, and fields.
Field definition structure:
| Property | Type | Description |
|---|---|---|
name | string | Field identifier (alphanumeric + underscore) |
description | string | Human-readable description |
type | string | string, int, float, bool, json, url, or datetime |
min | number | Minimum value/length constraint |
max | number | Maximum value/length constraint |
default | mixed | Default value |
fixed_list | array | Allowed values for dropdown-style fields |
can_be_null | boolean | Whether the field allows null values |
Request example:
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/metadata/templates/" \
-H "Authorization: Bearer {jwt_token}" \
-d 'name=Invoice+Template' \
-d 'description=Metadata+schema+for+invoices' \
-d 'fields=[{"name":"invoice_number","description":"Invoice number","type":"string","min":1,"max":50,"can_be_null":false},{"name":"amount","description":"Total amount","type":"float","min":0,"can_be_null":false}]'
Response (200 OK):
{
"result": true,
"template": {
"id": "mt_aBcDeFgHiJkLmN",
"instanceId": "1234567890123456789",
"orgId": "9876543210987654321",
"name": "Invoice Template",
"description": "Metadata schema for invoices",
"locked": false,
"priority": null,
"enabled": true,
"deleted": null,
"updated": "2025-01-20 10:30:00 UTC",
"created": "2025-01-20 10:30:00 UTC",
"fields": [
{
"name": "invoice_number",
"description": "Invoice number",
"type": "string",
"min": 1,
"max": 50,
"fixed_list": [],
"can_be_null": false
}
]
}
}
Newly created templates are active immediately — enabled is true for every template in current responses (there is no per-workspace enable/disable toggle).
Delete a template
/current/workspace/{workspace_id}/metadata/templates/{template_id}/
Soft-delete a metadata template. The delete is idempotent: deleting a template that no longer exists returns success. A template owned by a different workspace or organization (including system templates) is not visible to this endpoint and returns 404.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1609 (Not Found) | 404 | Template belongs to a different workspace or organization |
1664 (Datastore Error) | 500 | Deletion failed |
List templates
/current/workspace/{workspace_id}/metadata/templates/list/
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Optional path filter (append to URL): all (default, both system and custom), custom (non-system only), system (system only), enabled, or disabled.
Request example:
# List all templates
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/metadata/templates/list/" \
-H "Authorization: Bearer {jwt_token}"
# List only enabled templates
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/metadata/templates/list/enabled/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"count": 2,
"items": [
{
"id": "mt_aBcDeFgHiJkLmN",
"instanceId": "1234567890123456789",
"orgId": "9876543210987654321",
"name": "Contract Template",
"description": "Standard contract metadata fields",
"locked": true,
"priority": 3,
"enabled": true,
"deleted": null,
"updated": "2025-01-15 08:00:00 UTC",
"created": "2025-01-01 00:00:00 UTC",
"fields": [
{
"name": "contract_type",
"description": "Type of contract",
"type": "string",
"fixed_list": ["NDA", "MSA", "SOW"],
"can_be_null": false
}
]
}
]
}
Get template details
/current/workspace/{workspace_id}/metadata/templates/{template_id}/details/
Returns the full template with fields and workspace-level instance settings.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Update template definition
/current/workspace/{workspace_id}/metadata/templates/{template_id}/update/
Update a template’s field definitions. All body fields are optional — only provided fields are updated.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | No | Updated template name (1–100 characters) |
| description | string | No | Updated description (1–255 characters) |
| fields | string (JSON) | No | Updated JSON-encoded array of field definitions |
There is no category parameter.
Append /create/ to the path to copy the template instead of updating in place.
Response: Returns the updated template object. When the fields array changes in a way that invalidates stored values, the response additionally includes a schema_update object. Auto-triggered partial re-extraction runs only for: (a) new field names, and (b) type changes on existing names. Soft edits (description, min/max, nullable, fixed_list, regex) bump the schema hash but do not auto-trigger extraction — call the extract-all endpoint manually if you want those applied.
{
"result": true,
"template": { "...": "..." },
"schema_update": {
"previous_hash": 1234567890,
"current_hash": 2345678901,
"extraction_auto_enqueued": true,
"added_fields": ["currency"],
"type_changed_fields": ["total_amount"]
}
}
The schema_update object is absent when only name or description change.
Template-Node Mapping
Templates have many-to-many relationships with files. Use these endpoints to manage which files are mapped to which templates.
Eligible nodes
/current/workspace/{workspace_id}/metadata/eligible/
Paginated list of files and notes eligible for metadata extraction (nodes that have both an AI summary and a preview ready). Folders and links are excluded.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Number of items to return (1–500, default: 100) |
| offset | integer | No | Number of items to skip (default: 0) |
Add nodes to template
/current/workspace/{workspace_id}/metadata/templates/{template_id}/nodes/add/
Manually add nodes to a template. Both files and notes can be added; folders and links are rejected. Creates the many-to-many mapping between the template and the specified nodes.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| node_ids | string (JSON) | Yes | JSON-encoded array of node IDs to add to the template |
The number of files mapped to a single template is capped per plan tier. When the request would push the template above its cap, the endpoint returns 1605 (Invalid Input) with a message describing the cap, the attempted count, and the remaining slots. Concurrent calls cannot collectively exceed the cap.
Remove nodes from template
/current/workspace/{workspace_id}/metadata/templates/{template_id}/nodes/remove/
Remove nodes (files or notes) from a template. Removes the many-to-many mapping between the template and the specified nodes.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| node_ids | string (JSON) | Yes | JSON-encoded array of node IDs to remove from the template |
List nodes in template
/current/workspace/{workspace_id}/metadata/templates/{template_id}/nodes/
List nodes (files and notes) currently mapped to a template. The visible window is clamped to the workspace plan's per-template cap. When the template is truncated under the cap, rows are ordered by mapping creation order (oldest first) so the visible set is stable across plan changes. The default ordering when no sort_field is supplied is subject to change in upcoming releases; callers that depend on a specific ordering should pass sort_field explicitly.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Number of items to return (1–500, default: 100) |
| offset | integer | No | Number of items to skip (default: 0) |
| sort_field | string | No | Optional template field name to sort by |
| sort_dir | string | No | asc or desc (only with sort_field) |
Response (additional fields):
| Field | Type | Description |
|---|---|---|
| total_count | integer | Visible row count under the per-plan cap |
| total_count_unfiltered | integer | True row count in storage (ignores cap) |
| plan_node_limit | integer | Per-template node cap for the workspace's plan; -1 indicates unlimited / unresolved |
| is_truncated | boolean | true when overflow rows exist beyond the cap |
Pagination is clamped to the visible window: oversized limit values are silently shortened, and offsets past the visible window return an empty page. Existing pagination fields (items, count, next_cursor, has_more) are unchanged.
Preview template match
/current/workspace/{workspace_id}/metadata/templates/preview-match/
Synchronously preview which files would match a proposed metadata template before creating it. Accepts a template name and description, scans a sample of eligible files using AI classification, and returns the matched files with details. No template is created — this is a read-only preview operation.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Proposed template name (1–255 characters) |
| description | string | Yes | Proposed template description (1–2000 characters) |
Response (200 OK):
{
"result": true,
"matched_files": [
{
"node_id": "aBcDeFgHiJkLmN",
"name": "invoice_2025.pdf",
"mimetype": "application/pdf",
"summary_title": "Invoice from Acme Corp",
"summary_short": "Invoice INV-2025-001 for $1,500.00"
}
],
"total_eligible": 45,
"total_scanned": 45,
"total_matched": 1,
"plan_node_limit": 10,
"would_truncate_at": 1
}
| Field | Type | Description |
|---|---|---|
| matched_files | array | Files that matched the proposed template |
| matched_files[].node_id | string | File node opaque ID |
| matched_files[].name | string | File name |
| matched_files[].mimetype | string | MIME type |
| matched_files[].summary_title | string | AI-generated summary title |
| matched_files[].summary_short | string | AI-generated short summary |
| total_eligible | integer | Size of the eligible-file sample considered by this preview. When the workspace has more eligible files than fit in the sample window, this value is the sample ceiling (plus one, to signal "more available") rather than the true workspace-wide count |
| total_scanned | integer | Files actually scanned in this preview. May be smaller than total_eligible when the sample is bounded |
| total_matched | integer | Files that matched |
| plan_node_limit | integer | Per-template node cap for the workspace's plan; -1 indicates unlimited / unresolved |
| would_truncate_at | integer | Number of matches that would be persisted by a subsequent auto-match, given the current plan cap and the sampled matches. Equal to min(total_matched, plan_node_limit) when the cap applies, otherwise equal to total_matched. Because the preview only scans a sample, this is an estimate — a subsequent auto-match that scans the full workspace may match a different count. Frontends can show "X of Y matched — upgrade for more" when this value is less than total_matched. |
Requires available AI credits. Only files with completed AI summaries and previews are eligible.
Suggest template fields
/current/workspace/{workspace_id}/metadata/templates/suggest-fields/
Synchronously suggest a set of custom metadata fields for a proposed template, using AI. Accepts a small sample of eligible file node IDs (typically drawn from a prior preview-match call), a template description, and optional user intent text. Returns 1–10 suggested field definitions shaped to be used directly as the fields parameter of the create-template endpoint. No template is created — this is a read-only suggestion operation. Sits between preview-match and create-template in a template-creation flow.
Auth: Bearer token required. Workspace member. Metadata billing feature required. Rate limited; concurrent calls per user+workspace return 409 Conflict.
| Parameter | Type | Required | Description |
|---|---|---|---|
| node_ids | string (JSON) | Yes | JSON-encoded array of file node IDs to sample (1–25 items) |
| description | string | Yes | Template description (1–2000 characters) |
| user_context | string | No | Optional user intent string (max 64 characters, letters/numbers/spaces only) |
Response (200 OK):
{
"result": true,
"suggested_fields": [
{
"name": "Location",
"type": "string",
"description": "Geographic location where the content was created",
"max": 384,
"can_be_null": true,
"example_value": "New York, NY"
},
{
"name": "Year",
"type": "int",
"description": "Year the document was created or published",
"can_be_null": true,
"example_value": 2024
},
{
"name": "Priority",
"type": "string",
"description": "Priority level of the task",
"max": 384,
"can_be_null": false,
"fixed_list": ["low", "medium", "high"],
"example_value": "medium"
}
],
"file_sample_count": 25
}
| Field | Type | Description |
|---|---|---|
| suggested_fields | array | 1–10 suggested field definitions, directly compatible with create-template fields |
| suggested_fields[].name | string | Field display name |
| suggested_fields[].type | string | One of string, int, float, bool, datetime, url |
| suggested_fields[].description | string | Human-readable field description |
| suggested_fields[].max | number | Maximum length/value constraint (when applicable) |
| suggested_fields[].can_be_null | bool | Whether the field may be null |
| suggested_fields[].fixed_list | array | Allowed values for bounded categorical fields (status, priority, category). Present only when the field is enumerable. |
| suggested_fields[].example_value | mixed | Display-only hint showing what the extraction AI would likely return for this field, grounded in one of the sampled files. Type matches the field's type: string for string/url (HTML-stripped, max 256 chars), integer for int, number for float, boolean for bool, ISO 8601 string for datetime. May be omitted when no clean example is available. |
| file_sample_count | integer | Number of files actually used to generate suggestions |
Requires available AI credits. The json field type is not returned by this endpoint. example_value is a preview hint only and is not part of the create-template field schema; clients should strip it before posting to create-template (passing it through is harmless as unknown keys are ignored).
Auto-match files to template
/current/workspace/{workspace_id}/metadata/templates/{template_id}/auto-match/
Enqueues an async job that uses AI to match eligible files in the workspace to the template based on file content and template field definitions. The endpoint returns immediately with a job descriptor; results are written to the template asynchronously and progress can be polled via the jobs status endpoint under the template_match key.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Optional body parameters:
| Parameter | Type | Description |
|---|---|---|
| batch_size | integer | Optional batch size override; clamped server-side to the supported min/max range. Omit to use the default. |
Response (200 OK) — new job enqueued:
{
"result": true,
"job_id": "aj_aBcDeFgHiJkLmN",
"status": "queued",
"message": "Template matching job enqueued"
}
Response (200 OK) — duplicate job already running:
{
"result": true,
"status": "duplicate",
"message": "A template matching job is already in progress for this template"
}
| Field | Type | Description |
|---|---|---|
job_id | string | Async job ID. Present when status is queued; absent when status is duplicate. Use the jobs status endpoint to poll progress under the template_match key. |
status | string | queued for a newly enqueued job, or duplicate when an in-flight job for the same template was detected and re-used. |
message | string | Human-readable status message. |
The matched set is silently capped at the workspace's per-template node cap. Eligible files are truncated pre-batch (so no LLM credit is spent on candidates that cannot be persisted) and re-checked immediately before persistence to handle interleaved manual adds. When the template is already at its cap, the job completes successfully with no LLM usage.
Batch extract metadata (template-level)
/current/workspace/{workspace_id}/metadata/templates/{template_id}/extract-all/
Batch-extract metadata for all files mapped to a template. Async — returns a job_id for tracking.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Response (200 OK):
{
"result": true,
"job_id": "aj_aBcDeFgHiJkLmN",
"template_id": "mt_oPqRsTuVwXyZ12"
}
Maximum 1,000 files processed per job.
Template Assignments (folder-level)
Templates can be assigned to individual folder nodes (or workspace-level), independently of the per-file node map. These endpoints manage those folder assignments.
Assign a template to a folder
/current/workspace/{workspace_id}/metadata/template/assign/
Assign a metadata template to a folder node, or workspace-level when node_id is omitted. When a node_id is supplied, the node is added to the template’s node map under the same per-plan node cap that /nodes/add enforces.
Auth: Bearer token required. Workspace admin permission. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| template_id | string | Yes | Opaque template id to assign. Must belong to this workspace. |
| node_id | string | No | Folder node opaque id. Omit (or send empty) for a workspace-level assignment. |
Response (200 OK):
{
"result": true,
"node_id": "aBcDeFgHiJkLmN",
"template_id": "9876543210987654321"
}
node_id echoes what you sent (null for a workspace-level assignment). If the node is already mapped to the template the call succeeds as a no-op.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Missing/invalid template_id or node_id, or adding the node would exceed the per-plan template node cap |
1609 (Not Found) | 404 | Template not found, or does not belong to this workspace |
1658 (Not Acceptable) | 406 | Another assign for this template is in progress — retry shortly |
1664 (Datastore Error) | 500 | Cap check or assignment failed |
List template assignments
/current/workspace/{workspace_id}/metadata/template/assignments/
List the workspace’s (non-deleted) templates as assignment records.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Response (200 OK):
{
"result": true,
"count": 1,
"items": [
{
"template_id": "mt_aBcDeFgHiJkLmN",
"name": "Invoice Template",
"created": "2026-07-07 16:00:00 UTC",
"updated": "2026-07-07 16:30:00 UTC"
}
]
}
| Field | Type | Description |
|---|---|---|
count | integer | Number of assignment records returned |
items[].template_id | string | Opaque template id |
items[].name | string | Template name |
items[].created | string | Creation timestamp (YYYY-MM-DD HH:MM:SS UTC) |
items[].updated | string | Last update timestamp (YYYY-MM-DD HH:MM:SS UTC) |
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1664 (Datastore Error) | 500 | Failed to list template assignments |
Resolve the effective template for a node
/current/workspace/{workspace_id}/metadata/template/resolve/{node_id}/
Resolve the metadata template mapped to a storage node by walking the node map. Returns the first mapped template, or null when the node has no mapping.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
{node_id} is a path segment (a storage node opaque id).
Response (200 OK):
{
"result": true,
"template": {
"id": "mt_aBcDeFgHiJkLmN",
"name": "Invoice Template",
"fields": []
}
}
When the node has no mapped template the response is { "result": true, "template": null }. The template object, when present, is the full template resource (see Template Resource Schema).
Unassign a template from a folder
/current/workspace/{workspace_id}/metadata/template/unassign/
Remove a template assignment from a folder node. Because this is a DELETE, pass the parameters as query string (a DELETE body is not read).
Auth: Bearer token required. Workspace admin permission. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| node_id | string | No | Folder node opaque id (query param). Omit for the workspace-level no-op. |
| template_id | string | Conditional | Required when node_id is supplied. |
Response (200 OK):
{ "result": true }
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Invalid node_id, or missing/invalid template_id when a node_id is supplied |
1664 (Datastore Error) | 500 | Failed to remove the assignment |
Node Metadata
Metadata stored on individual files. Split into template metadata (conforming to mapped templates) and custom metadata (user-defined fields).
Get file metadata
/current/workspace/{workspace_id}/storage/{node_id}/metadata/details/
Returns all metadata for a file. Response contains template_metadata and custom_metadata separately.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Request example:
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/storage/aBcDeFgHiJkLmN/metadata/details/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK):
{
"result": true,
"instance_id": "1234567890123456789",
"object_id": "aBcDeFgHiJkLmN",
"template_id": "mt_oPqRsTuVwXyZ12",
"node_id": {
"id": "aBcDeFgHiJkLmN",
"name": "invoice_2025.pdf",
"type": "file",
"size": 245760
},
"metadata": [
{
"key": "invoice_number",
"description": "Invoice number",
"type": "string",
"value": "INV-2025-001",
"is_auto": false,
"updated": "2025-01-20 10:30:00 UTC"
},
{
"key": "amount",
"description": "Invoice amount",
"type": "float",
"value": 1500.00,
"is_auto": true,
"updated": "2025-01-20 10:30:00 UTC"
}
]
}
| Field | Type | Description |
|---|---|---|
response.template_id | string/null | Associated template ID, or null |
response.node_id | object | Storage node resource with file details |
response.metadata[].key | string | Metadata field key |
response.metadata[].type | string | Value type (string, int, float, bool, json, url, datetime) |
response.metadata[].value | mixed | The metadata value |
response.metadata[].is_auto | boolean | Whether the value was auto-generated by AI extraction |
response.metadata[].updated | string | Last update timestamp |
Bulk Form
The metadata details endpoint accepts a comma-separated list of node ids in place of a single {node_id}:
GET /current/workspace/{workspace_id}/storage/{id1},{id2},{id3}/metadata/details/
Up to 25 ids per call. Duplicate ids are silently deduplicated. Empty segments (e.g. trailing comma) return 400.
The bulk response shape differs from the single-id form. Successfully resolved objects appear under objects; per-id failures appear in a parallel errors array. Template definitions are hoisted to a top-level templates map keyed by template_id so a template shared by N objects is returned once instead of N times — clients look up each object's template via the template_id it carries. HTTP status is 200 when at least one object resolves and 404 when every requested id errored.
curl Example
curl -X GET "https://api.fast.io/current/workspace/1234567890123456789/storage/abc123,def456,ghi789/metadata/details/" \
-H "Authorization: Bearer {jwt_token}"
Response (HTTP 200)
{
"result": true,
"format": "multi",
"objects": [
{
"instance_id": "1234567890123456789",
"object_id": "abc123",
"template_id": "mt_oPqRsTuVwXyZ12",
"node_id": { "id": "abc123", "name": "invoice_2025.pdf", "type": "file" },
"template_metadata": [
{ "key": "invoice_number", "description": "Invoice number", "type": "string", "value": "INV-2025-001", "is_auto": false, "updated": "2025-01-20 10:30:00 UTC" }
],
"custom_metadata": [],
"autoextractable": true
},
{
"instance_id": "1234567890123456789",
"object_id": "def456",
"template_id": "mt_oPqRsTuVwXyZ12",
"node_id": { "id": "def456", "name": "invoice_2026.pdf", "type": "file" },
"template_metadata": [
{ "key": "invoice_number", "description": "Invoice number", "type": "string", "value": "INV-2026-014", "is_auto": true, "updated": "2026-02-04 09:12:00 UTC" }
],
"custom_metadata": [],
"autoextractable": true
}
],
"templates": {
"mt_oPqRsTuVwXyZ12": {
"template_id": "mt_oPqRsTuVwXyZ12",
"name": "Invoice",
"fields": [
{ "name": "invoice_number", "type": "string", "description": "Invoice number" }
]
}
},
"errors": [
{ "node_id": "ghi789", "code": 191049, "message": "Storage node not found" }
]
}
errors is always an array (possibly empty); templates is always an object/map (possibly empty {}).
Request-level Errors (whole request fails)
| Error Code | Sub-code | HTTP Status | Description |
|---|---|---|---|
1605 (Invalid Input) | 160655 | 406 | Empty segment between commas |
1605 (Invalid Input) | 109184 | 406 | More than 25 unique ids in one request |
1609 (Not Found) | — | 404 | Every requested id errored (errors array is populated) |
Per-id Error Codes (inside each errors[] entry)
The request itself is HTTP 200 unless every id errored. Each entry in errors[] carries one of:
| Code | Meaning |
|---|---|
147196 | Invalid storage node id format |
196136 | The literal root sentinel was supplied (only files/notes are valid) |
191049 | Storage node not found |
190770 | Backend error retrieving the storage node (any non-not-found failure) |
150183 | Storage node exists but is not a file or note (e.g. a folder) |
157684 | Backend failure retrieving the metadata key/value rows |
Update file metadata
/current/workspace/{workspace_id}/storage/{node_id}/metadata/update/{template_id}/
Set or update metadata key-value pairs on a file.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| key_values | string (JSON) | Yes | JSON-encoded object of key-value pairs. Keys must match template field names. |
Request example:
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/storage/aBcDeFgHiJkLmN/metadata/update/mt_oPqRsTuVwXyZ12/" \
-H "Authorization: Bearer {jwt_token}" \
-d 'key_values={"invoice_number":"INV-2025-001","amount":1500.00}'
Existing metadata for the same keys is overwritten. Keys not included in the request are left unchanged.
Delete file metadata
/current/workspace/{workspace_id}/storage/{node_id}/metadata/
Delete metadata keys from a file.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| keys | string (JSON) | No | JSON-encoded array of key names to delete. If omitted, all keys may be removed. |
Only files and notes support metadata — folders return an error.
Extract metadata (single file)
/current/workspace/{workspace_id}/storage/{node_id}/metadata/extract/
Enqueues an AI extraction job for a single file against a template. Asynchronous — returns HTTP 202 Accepted with a job descriptor. Poll GET /current/workspace/{workspace_id}/jobs/status/ to track progress, then read the values from GET /current/workspace/{workspace_id}/storage/{node_id}/metadata/details/ once the job reports completed. Supports documents, spreadsheets, images (PNG, JPEG, WebP), and code files. Extracted values are stored with is_auto: true.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| template_id | string | No | The template ID to extract against. Defaults to the first template mapped to the file. |
| fields | string (JSON) | No | JSON-encoded array of field names to restrict extraction scope. Each name must exist on the template. Omit to extract the full schema. |
Response (HTTP 202 Accepted):
{
"result": true,
"job_id": "{job_id}",
"template_id": "{template_id}",
"node_id": "{node_id}",
"fields": ["amount", "due_date"],
"status": "queued",
"status_uri": "/workspace/{workspace_id}/jobs/status"
}
status is queued for a fresh job, or in_progress when the request is collapsed into an already-running job (see idempotent retries below).
Idempotent retries: submitting the same (node, template, fields) combination while a job is already in flight returns the existing job_id instead of enqueueing a duplicate. Retries within a 30-minute window are safe.
Polling flow:
POST /current/workspace/{id}/storage/{node}/metadata/extract/
template_id={template_id}
-> HTTP 202
{ "job_id": "{job_id}", "status": "queued",
"status_uri": "/workspace/{id}/jobs/status" }
GET /current/workspace/{id}/jobs/status/
-> jobs.metadata_extract[] includes
{ "kind": "single",
"active": true,
"node_id": "{node_id}",
"template_id": "{template_id}",
"job_id": "{job_id}",
"status": "in_progress",
"progress_percent": 0 }
GET /current/workspace/{id}/jobs/status/ (later)
-> entry now { "status": "completed",
"progress_percent": 100,
"completed_at": ... }
GET /current/workspace/{id}/storage/{node}/metadata/details/
-> extracted values under `template_metadata`
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Node is root, fields references an unknown field, or payload is malformed |
1609 (Not Found) | 404 | Template not found |
1609 (Not Found) | 404 | Node not found |
1664 (Datastore Error) | 500 | Failed to enqueue extraction job |
1696 (Credits Exhausted) | 402 | No AI credits remaining |
Batch extract metadata for a folder
/current/workspace/{workspace_id}/storage/{node_id}/metadata/extract-all/
Enqueue an async job that runs metadata extraction on every file in a folder against the workspace’s template. This is the folder-level counterpart to the single-file /metadata/extract/ (one file) and the template-level /metadata/templates/{template_id}/extract-all/ (all files mapped to one template).
Auth: Bearer token required. Workspace admin permission. Metadata billing feature required. Conservative throttle (these operations are expensive).
{node_id} is a folder node opaque id, or the literal root alias for the workspace root.
Response (200 OK):
{
"result": true,
"job_id": "aj_aBcDeFgHiJkLmN",
"template_id": "mt_oPqRsTuVwXyZ12"
}
The job runs asynchronously against the workspace’s active template — poll GET /current/workspace/{workspace_id}/jobs/status/ (the jobs.metadata_extract[] array) for progress.
Error responses:
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Missing/invalid folder id, or the node is not a folder |
1609 (Not Found) | 404 | Folder not found, or no template is configured for the workspace |
1664 (Datastore Error) | 500 | Failed to list templates or enqueue the job |
List metadata by template
/current/workspace/{workspace_id}/storage/{node_id}/metadata/list/{template_id}/
List all files with metadata for a specific template. Supports filtering and sorting.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| filters | string (JSON) | No | JSON-encoded filter criteria |
| order_by | string | No | Field key name to sort by |
| order_desc | string | No | "true" or "false" for descending sort |
List templates in use
/current/workspace/{workspace_id}/storage/{node_id}/metadata/templates/
List templates that have metadata set on files, with object counts.
Auth: Bearer token required. Workspace member. Metadata billing feature required.
Metadata versions
/current/workspace/{workspace_id}/storage/{node_id}/metadata/versions/
List metadata version snapshots for a file. Useful for tracking changes to extracted metadata over time.
Metadata Saved Views
Each user can persist per-template column order, column visibility, and sort preferences. Saved views are private to the user — two users can independently customize the same template. Identity is the tuple (workspace, user, template); there is at most one saved view per user per template per workspace. Full endpoint reference is in the Storage Operations docs.
/current/workspace/{workspace_id}/metadata/view/?template_id={template_id}
/current/workspace/{workspace_id}/metadata/view/
/current/workspace/{workspace_id}/metadata/view/?template_id={template_id}
/current/workspace/{workspace_id}/metadata/views/
/current/workspace/{workspace_id}/metadata/view/{template_id}/export/
Auth required. Workspace member. Metadata billing feature required.
POST body: application/x-www-form-urlencoded with template_id (string) and config (JSON-encoded string containing the view config). Do NOT send as application/json — the endpoint only accepts form-encoded bodies and returns 406 (This field is missing) for JSON bodies. The config schema (the JSON serialized into the form field) is { version: 1, columns: [{field, visible?, width?}], sort: {field, dir}, filters: [{field, operator, value_type, value}] }; column array order IS the display order; dir is asc or desc; filters are AND-chained, max 5 per view, with operator in = != < <= > >= and value_type in string | int | float | bool (json rejected). Each saved-view filter entry uses field for its template-field identifier (matching columns[].field and sort.field); the nodes endpoint's stateless filters query parameter keeps its pre-existing key form, and the server handles the translation when auto-applying a saved view. Unknown keys are rejected with 1605 (Invalid Input). See the Storage Operations docs for the full filter contract.
The template nodes listing endpoint (GET /workspace/{workspace_id}/metadata/templates/{template_id}/nodes) auto-applies config.sort and config.filters from the caller's saved view when the corresponding sort_field / filters query parameters are not passed explicitly. An explicit query parameter (even empty) always wins; no server-side merge.
A saved view can also be exported to a TSV file: POST /workspace/{workspace_id}/metadata/view/{template_id}/export/ queues an asynchronous export written to workspace storage, applying the view's columns, sort, and filters. A saved view must already exist for that template. The export runs in the background — poll the destination folder for the resulting file. See the Storage Operations docs for the export parameters, deduplication behavior, and row limit.
Jobs Status (Unified Async Processing)
A single endpoint to check the status of all async processing jobs (AI indexing, metadata extraction) for a workspace or share. Replaces the removed metadata/intelligence/status and metadata/templates/{id}/extract-status endpoints.
/current/workspace/{workspace_id}/jobs/status/
Workspace jobs status. Auth: Workspace member. Feature gate: AI feature must be enabled on the organization plan.
/current/share/{share_id}/jobs/status/
Share jobs status. Auth: Share viewer. Feature gate: AI feature must be enabled on the organization plan.
Response (200 OK):
{
"result": true,
"jobs": {
"intelligence": {
"active": true,
"status": "ingesting",
"direction": "enable",
"total_files": 100,
"eligible_files": 80,
"processed": 30,
"skipped": 5,
"failed": 0,
"progress_percent": 37,
"started_at": 1711500000,
"updated_at": 1711500300,
"completed_at": null,
"stop_reason": null
},
"metadata_extract": [
{
"kind": "batch",
"active": true,
"template_id": "1234567890123456789",
"node_id": null,
"job_id": "{batch_job_id}",
"status": "extracting",
"total_files": 50,
"eligible_files": 40,
"processed": 24,
"skipped": 2,
"failed": 0,
"progress_percent": 60,
"started_at": 1711500000,
"updated_at": 1711500200,
"completed_at": null,
"stop_reason": null,
"error_message": null,
"fields_scope": ["name", "date", "amount"]
},
{
"kind": "single",
"active": true,
"template_id": "1234567890123456789",
"node_id": "{node_id}",
"job_id": "{job_id}",
"status": "in_progress",
"total_files": 1,
"eligible_files": 1,
"processed": 0,
"skipped": 0,
"failed": 0,
"progress_percent": 0,
"started_at": 1711500400,
"updated_at": 1711500400,
"completed_at": null,
"stop_reason": null,
"error_message": null,
"fields_scope": ["amount", "due_date"]
}
]
}
}
| Field | Type | Description |
|---|---|---|
jobs.intelligence | object/null | AI indexing job status, or null if no job exists |
jobs.intelligence.status | string | starting, ingesting, flushing, draining, completed, failed, or stopped |
jobs.intelligence.direction | string | enable (indexing files) or disable (removing embeddings) |
jobs.intelligence.progress_percent | integer | 0–100 progress based on processed/eligible |
jobs.metadata_extract | array | Mixed extraction statuses (empty array if none). Each entry is either a per-template batch job or a per-node single-file job. |
jobs.metadata_extract[].kind | string | "batch" for per-template batch extraction, "single" for per-node single-file extraction |
jobs.metadata_extract[].node_id | string/null | Node identifier for kind: "single" entries; null for kind: "batch" |
jobs.metadata_extract[].status | string | Batch: queued, starting, walking, extracting, or completed states. Single: queued, in_progress, completed, or errored. |
jobs.metadata_extract[].fields_scope | array/null | Template field names being extracted, or null for full-schema extraction |
Both intelligence and extraction entries share the common progress fields (active, total_files, eligible_files, processed, skipped, failed, progress_percent, started_at, updated_at, completed_at, stop_reason). For kind: "single" entries, total_files and eligible_files are always 1 and progress_percent is 0 while pending or 100 on completion. Completed or errored entries older than one hour are hidden from the listing.
Real-time updates: Both job types broadcast via the Activity/WebSocket system. Clients subscribed to the workspace or share WebSocket channel receive activity notifications when progress changes, reducing the need for polling.
Template Resource Schema
| Field | Type | Description |
|---|---|---|
id | string | Opaque template identifier |
instanceId | string | Workspace ID |
orgId | string/null | Organization ID |
name | string | Template name |
description | string | Template description |
locked | boolean | Whether the template is locked from editing |
priority | integer/null | Priority level 1–5, or null |
enabled | boolean | Whether enabled for the workspace |
deleted | string/null | Soft-delete timestamp, or null |
updated | string | Last updated timestamp |
created | string | Creation timestamp |
fields | array | Array of field definition objects |
node_count | integer | Number of files mapped to the template |
field_count | integer | Number of fields declared in the template schema |
The template resource does not include a category field.
Supported Field Types
| Type | Description |
|---|---|
string | Text values (max 10,000 characters) |
int | Integer numbers |
float | Decimal numbers |
bool | Boolean true/false |
json | Complex JSON structures |
url | Validated URLs |
datetime | Date and time values |
Quick Reference
Create a chat and get the answer
POST /current/workspace/{id}/ai/agent/
question=...
-> thread.thread_id, turn.turn_id
GET /current/activity/poll/{id}?wait=95&lastactivity=...
-> wait for ai_chat:{chatId}
GET /current/workspace/{id}/ai/agent/{chat_id}/message/{turn_id}/details/
-> check status == complete
GET /current/workspace/{id}/ai/agent/{chat_id}/message/{turn_id}/read/
-> SSE stream: data, commentary, status, analysis_data, table_data, done
Create a note (bank knowledge for RAG)
POST /current/workspace/{id}/storage/{parent}/createnote/
name=research-notes.md&content=...
Extract metadata from a file (async)
POST /current/workspace/{id}/storage/{node}/metadata/extract/
template_id={template_id}
-> HTTP 202 { job_id, status: "queued" | "in_progress" }
GET /current/workspace/{id}/jobs/status/
-> jobs.metadata_extract[] (kind: "single") reaches status: "completed"
GET /current/workspace/{id}/storage/{node}/metadata/details/
-> extracted values
List eligible nodes for metadata (files and notes)
GET /current/workspace/{id}/metadata/eligible/
Add nodes to a template (files and notes)
POST /current/workspace/{id}/metadata/templates/{template_id}/nodes/add/
node_ids=["nodeId1","nodeId2"]
AI auto-match files to a template
POST /current/workspace/{id}/metadata/templates/{template_id}/auto-match/
Batch extract metadata for all files in a template
POST /current/workspace/{id}/metadata/templates/{template_id}/extract-all/
-> returns job_id (rate limited)
Check status of all async jobs (intelligence + metadata extraction)
GET /current/workspace/{id}/jobs/status/
GET /current/share/{id}/jobs/status/
-> returns jobs.intelligence and jobs.metadata_extract[]
Semantic search (use /storage/search instead — /ai/search is deprecated)
GET /current/workspace/{id}/storage/search/?search=quarterly+revenue&limit=10
GET /current/workspace/{id}/storage/search/?search=quarterly+revenue&details=true
Optional details=true includes full node resource (previews, AI state, metadata, size) per result. Default limit drops to 10 when details enabled.
Share AI chat (same workflow as workspace)
POST /current/share/{id}/ai/agent/
question=...
-> thread.thread_id, turn.turn_id
GET /current/share/{id}/ai/agent/{chat_id}/message/{turn_id}/details/
-> check status == complete
GET /current/share/{id}/ai/agent/{chat_id}/message/{turn_id}/read/
-> SSE stream: data, commentary, status, analysis_data, table_data, done
Share-specific AI
GET /current/share/{id}/ai/autoog/
-> binary PNG image (OG image)
POST /current/share/{id}/ai/autotitle/
-> title, description, display_type
POST /current/share/{id}/ai/share/
files=["opaqueId1","opaqueId2"]
-> markdown with download URLs
↑ Back to top