# Fastio > Put your files to work — workspaces for agentic teams. Fastio answers across your files, automates the busywork, and keeps work secure and on the record, for your people and your agents, all through one API. Storage is step zero. > > Base URL: https://api.fast.io/current/ > Version: 1.0 > Request format: `application/x-www-form-urlencoded` (most POST bodies) or query string (GET). Some endpoints accept `application/json` bodies — notably comments and endpoints with nested/array parameters. > Response format: JSON > File uploads: `multipart/form-data` > Document version: 2.3 > Last updated: 2026-08-10 > Full reference (single file): https://api.fast.io/current/llms/full/ > Agent integration guide: available at the `/current/agents/` endpoint on the connected API server > MCP Server (AI Agents): connect via the Model Context Protocol for tool access > MCP Streamable HTTP endpoint: https://mcp.fast.io/mcp > MCP Legacy SSE endpoint: https://mcp.fast.io/sse > MCP Skills & Tool Definitions: available at the `/skill.md` endpoint on the connected MCP server Fastio provides workspaces for agentic teams — where agents collaborate with other agents and with humans. Upload outputs, create branded portals, ask questions about documents using built-in AI, and hand everything off to a human when the job is done. No infrastructure to manage. **For AI agents:** Fastio accounts — for humans and AI agents alike — require an email address. Sign up, create an organization, and choose a paid plan to get started, then build workspaces for your team (agents and humans) and query documents with built-in RAG. The optional `agent=true` flag tags your account as an agent account (`account_type=agent`) for identification. See the [Agent Integration Guide](https://api.fast.io/current/agents/) for workflows and examples. **For MCP-enabled agents:** Connect via the Model Context Protocol to interact with Fastio workspaces, shares, files, and storage directly. Connect to `https://mcp.fast.io/mcp` (Streamable HTTP) or `https://mcp.fast.io/sse` (legacy SSE). In Named Mode (Claude Desktop, etc.), the server exposes multiple domain-specific tools plus app-specific widget tools; in Code Mode (Claude Code, Cursor, etc.), a smaller set of streamlined tools. All tools use action-based routing and are annotated with MCP hints (`title`, `readOnlyHint`, `destructiveHint`). Resources (`skill://guide`, `session://status`) and guided prompts for common workflows are also available. The MCP server provides a `/skill.md` endpoint with available tools and skill definitions. ## Detailed API References This file is a concise overview. For complete endpoint documentation with parameters, response fields, and examples, see the category-specific references: | Category | URL | What It Covers | |----------|-----|----------------| | **Auth & Users** | https://api.fast.io/current/llms/auth/ | Authentication methods, user CRUD, getting started patterns | | **OAuth 2.0** | https://api.fast.io/current/llms/oauth/ | PKCE flow, token exchange, session management, DCR, resource indicators | | **Organizations** | https://api.fast.io/current/llms/orgs/ | Org CRUD, members, billing, discovery | | **Single Sign-On** | https://api.fast.io/current/llms/sso/ | Organization identity-provider configuration (OIDC and SAML), DNS domain verification, configuration checks, sign-in modes | | **Workspaces** | https://api.fast.io/current/llms/workspaces/ | Workspace CRUD, members, assets, discovery | | **Storage** | https://api.fast.io/current/llms/storage/ | File/folder operations, locking, previews, transforms | | **Shares** | https://api.fast.io/current/llms/shares/ | Share types, storage modes, members, branding, durable File Share (single-file links; supersedes the deprecated QuickShare) | | **Upload** | https://api.fast.io/current/llms/upload/ | Chunked upload flow, web upload, status polling | | **AI & Chat** | https://api.fast.io/current/llms/ai/ | RAG chat, intelligence, notes, saved metadata filters & extraction | | **How-To** | https://api.fast.io/current/llms/howto/ | Single-call natural-language "how do I…" product guidance (answer or clarifying question) | | **Events & Activity** | https://api.fast.io/current/llms/events/ | Event search, activity polling, WebSocket (with permission-scoped enriched event push), realtime | | **Comments** | https://api.fast.io/current/llms/comments/ | Threading, mentions, reactions, JSON body format | | **Signing / E-Signature** | https://api.fast.io/current/llms/signing/ | SignEnvelopes, recipients, fields, signer surface (OTP, consent, sign/decline), audit chain + certificate, PAdES-LT signing, provider webhooks, plan-gated availability; sign templates (reusable configurations — create/list/details/update/delete/instantiate) | | **Dashboard** | https://api.fast.io/current/llms/dashboard/ | Per-workspace actionable card feed (mentions, file activity, and pending signatures) with optional AI overlay (urgency, summaries, synthesis), dismiss/snooze, and Ripley Agent seed handoff | | **Agent Intents** | https://api.fast.io/current/llms/intents/ | Workspace-scoped, short-lived "what am I doing" declarations for coordinating agents — allocate/fill/browse/expand/release, credential-keyed slots with user-scoped ownership, compare-and-set writes | | **Full Reference** | https://api.fast.io/current/llms/full/ | All endpoints in a single file | ## What Fastio Does Storage is step zero. Fastio is the workspace platform for agentic teams — where agents work with other agents and with humans — organized around three things you do with your files: | Pillar | What It Does | |--------|--------------| | **Intelligence** | Ask across all your files and get cited answers (RAG chat); turn documents and images into structured data (AI metadata extraction, organised with saved metadata filters); research, analyze, and draft with the built-in agent, Ripley; semantic search by meaning | | **Automation** | Automate work on your files with event or schedule triggers — run agents (Ripley or your own, over MCP) against documents, route files, and collect sign-offs | | **Secure collaboration** | Files belong to the project, not the person — org/workspace-owned storage, Send/Receive/Exchange shares and branded portals, granular permissions and scoped agent tokens, an append-only audit log, and a per-workspace dashboard of what needs attention | **Building blocks:** | Capability | What It Does | |-----------|-------------| | **Workspaces** | Shared workspaces for agentic teams with file versioning, search, and AI chat | | **Shares** | Purpose-built spaces for agent-human exchange with two storage modes: **Portal** (independent portal with passwords, expiration, guest access, download security levels) or **Shared Folder** (live-synced workspace folder). Three share types: Send, Receive, Exchange. Download security: `high` (disabled), `medium` (nonce-gated), `off` (unrestricted). | | **Built-in AI (Ripley)** | RAG-powered document Q&A with citations, semantic search (vector retrieval without LLM), auto-summarization, metadata extraction | | **File Preview** | Inline rendering for PDF, images, video, audio, spreadsheets, code — no download needed | | **Activity Tracking** | Full audit trail with AI-powered natural language summaries | ## Plans New organizations choose a paid plan (Starter, Business, Growth, or Enterprise). A newly created organization must select a paid plan before it can be used; until then it is in an upgrade-only state (the same state as an org that has exhausted its credits — gated endpoints return HTTP 402). Each plan includes a monthly credit allowance; credits cover: storage (150/GB), bandwidth (400/GB), AI tokens (1/100 tokens), document ingestion (10/page), video ingestion (5/sec), image ingestion (5/image), file conversions (25/each). **To activate or upgrade an org:** Direct the user to `https://fast.io` or use `POST /current/org/{org_id}/billing/` to select a paid plan. See [Organizations reference](https://api.fast.io/current/llms/orgs/) for details. **Enterprise** is a self-serve plan at $899 per month, bought through the normal checkout like any other tier, that adds org-level Single Sign-On, SCIM directory provisioning and the org security controls — including the option to enforce SSO as the only sign-in path for the org's verified domains. It includes 15,000,000 credits and 150 seats per month and is billed monthly only. See [Single Sign-On reference](https://api.fast.io/current/llms/sso/) for details. ## Profile Hierarchy User (Type 2) → Organization (Type 3) → Workspace (Type 4) / Share (Type 5) Users own organizations. An organization is a collector of workspaces — it can represent a company, a business unit, a team, or simply a personal collection. Organizations own workspaces and shares. Users can also directly own shares. All profile IDs are 19-digit numeric strings (e.g., "2234567890123456789"). The leading digit encodes the profile type — `2` = user, `3` = org, `4` = workspace, `5` = share. Most endpoints also accept a custom name in place of the numeric ID — see ID Formats below. Account types: `human` or `agent` — visible in all user objects via `account_type` field. ## Authentication All authenticated endpoints require: `Authorization: Bearer {jwt_token}` Four methods: - **Basic Auth → JWT:** `GET /current/user/auth/` with HTTP Basic Auth. Returns JWT. If 2FA enabled, token has limited scope until verified. - **OAuth 2.0 PKCE:** For desktop/mobile apps and MCP agents. S256 only. Supports Dynamic Client Registration (RFC 7591), Client ID Metadata Document (CIMD) for URL-based client_id, and Resource Indicators (RFC 8707). See [OAuth reference](https://api.fast.io/current/llms/oauth/). - **API Keys:** Long-lived tokens. Same Bearer header. Create via `POST /current/user/auth/key/`. Keys optionally support scoped permissions (`scopes`), agent names (`agent_name`), and expiration (`expires`). Update via `POST /current/user/auth/key/{id}/` (POST, not PUT). A scope is `entity_type:entity_id:access_mode`, and there are three access modes: `r` (read), `rw` (read and write) and `rwa` (read, write and **administer**). `rwa` implies `rw` implies `r`; there is no `ra`; and admin is re-checked per request against the human's live role. A key created without `scopes` now stores the explicit `["user:*:rw"]` — whole-account read and write, but **no administration and no account settings**; those need `rwa` scopes and the `userdetails:*:rw` scope respectively, and a credential cannot widen itself. → [Auth reference](https://api.fast.io/current/llms/auth/) - **2FA:** Limited-scope token → full token after `POST /current/user/auth/2factor/auth/{token}/`. **Choose your access pattern:** 1. **Human's account** — Human creates API key, gives it to you. You operate as them. → [Auth reference](https://api.fast.io/current/llms/auth/) 2. **Your own account** — Sign up (email + password, optionally `agent=true`), create an org, select a paid plan, work independently. → [Auth reference](https://api.fast.io/current/llms/auth/) 3. **Collaboration** — Sign up, then a human invites you to their org/workspace. → [Auth reference](https://api.fast.io/current/llms/auth/) 4. **PKCE browser login** — Secure, no password sharing, supports SSO. → [OAuth reference](https://api.fast.io/current/llms/oauth/) ## Response Envelope Success (data fields at root level): ```json {"result": true, ...} ``` Error: ```json {"result": false, "error": {"code": 195654, "text": "Human-readable message", "documentation_url": "https://api.fast.io/llms.txt", "resource": "POST /current/user/"}} ``` Validation error (HTTP 406) with structured per-parameter detail: ```json {"result": false, "error": {"code": 10022, "text": "email: This value should not be blank.", "documentation_url": "https://api.fast.io/llms.txt", "resource": "POST /current/user/email/", "params": [{"name": "email", "kind": "missing", "message": "This value should not be blank.", "code": 10022}]}} ``` - `result`: boolean — `true` on success, `false` on error - `error.code`: Unique error identifier (integer) for debugging - `error.text`: Human-readable error message. Advisory; clients should prefer `params` for programmatic handling. Retained byte-identically for compatibility. - `error.documentation_url`: Link to error documentation (string or null) - `error.resource`: The endpoint that produced the error - `error.params`: Array of `{name, kind, message, code, expected_type?, received_alias?}`. Present on validation errors (HTTP 406) and on some conflicts (HTTP 409). Aggregates every failed parameter so callers see all problems in one round trip. `kind` is one of `missing` (required parameter omitted), `invalid` (value failed a constraint), `type_mismatch` (value could not be decoded into the expected type), `unknown_parameter` (the parameter is not part of the accepted set for this request — stop sending it rather than correcting its value), or `conflict` (the value was well formed, but the state it referred to has since changed — see below). Omitted when empty. **Treat `kind` as open-ended: handle an unrecognised value as a generic failure rather than rejecting the response.** - **Conflict entries (HTTP 409).** A `conflict` entry names the parameter whose precondition no longer holds and carries a `reason` naming the cause — currently `conflict_version_mismatch`, meaning the resource changed since the version you supplied. Branch on `params[].reason` — the only field that names the cause. `params[].name` + `params[].kind` is a fallback for clients receiving only the four standard fields: it identifies *a* failed precondition on that parameter, not which one, so treat it as a generic conflict. Do **not** branch on the HTTP status (`409` reports several unrelated conditions) or on the numeric code (assigned per call site, so it differs between endpoints reporting the same cause). Re-read the resource and re-apply your change against the current state; resending the same request unchanged cannot succeed. **That last point is about `conflict` entries, not about `409` in general** — a `409` carrying no `params[]` can report a condition that clears on its own, and the cloud-sync write-back queue has one (see Cloud Sync below). Every `reason` value is lowercase snake_case, so a client can normalise once over the field rather than special-casing each condition. Note the asynchronous upload session reports the same condition through `session.status_message` as `CONFLICT_VERSION_MISMATCH:{current_version_id}` — upper-case, and a prefix rather than a whole value. The two are matched by different code and are never compared, so the casing difference is deliberate rather than an inconsistency to normalise away. OAuth (`/current/oauth/token/`, `/current/oauth/revoke/`, `/current/oauth/register/`) and the cloud-storage / billing webhook receivers retain RFC-compliant bare-JSON error envelopes (RFC 6749 §5.2 / RFC 7591). The `params` field is **not** emitted by those endpoints. ## OPTIONS Introspection `OPTIONS` introspection is currently supported on roughly 40% of public endpoints. Supported endpoints respond with a JSON description of their accepted parameters — source (query / body / path / header), required vs optional flag, expected type, and a summary of declared constraints (length bounds, choice set, range, equality). Use this to fetch parameter requirements before issuing a call rather than learning them from a validation-error round trip. ```bash curl -X OPTIONS "https://api.fast.io/current/{endpoint}/" -H "Authorization: Bearer {jwt_token}" ``` Unsupported endpoints (OAuth, webhook receivers, and a substantial set of legacy endpoints not yet migrated) return `405 Method Not Allowed` for `OPTIONS`. Coverage is being expanded incrementally — check via `OPTIONS` first; fall back to the documented schema when you receive a 405. ## Error Codes **Reading this list:** the four-digit `16xx`/`17xx` values below are **HTTP-status classes, not `error.code`**. The `error.code` a client actually receives is assigned per endpoint, so **use the HTTP status as the gate and a documented `error.code` — five or six digits, plus the `9661`-`9669` family — only as a refinement**. A `16xx` value identifies the status class — useful for telling which kind of failure occurred — but comparing one against `error.code` will never match. Codes shown as five or six digits (and the `9661`-`9669` family) ARE `error.code` values. **If you widen a check from a specific code to a status, widen what you assert with it** — a status covers failures the narrower code did not, so a message written for that one code becomes a confident falsehood on the rest. Client errors: - 1605 (Invalid Input) → 406 Not Acceptable - 1607 (Duplicate Entry) → 406 Not Acceptable - 1650 (Authentication Invalid) → 401 Unauthorized - 1651 (Invalid Request Type) → 405 Method Not Allowed - 1609 (Not Found) → 404 Not Found - 1652 (Resource Not Found) → 404 Not Found - 1653 (User Not Found) → 404 Not Found - 1701 (Gone) → 410 Gone — the endpoint was retired by decision. Distinct from 404 on purpose: 404 means "no such thing" and is answered by re-checking the id or your access, whereas 410 means the path itself is finished. Do not retry it, do not vary the id, and do not treat it as an outage — remove the call and use the replacement named in the error message. - 1656 (Limit Exceeded) → 413 Payload Too Large - 1667 (Max Limit) → 429 Too Many Requests - 1685 (Feature Limit) → 412 Precondition Failed - 1658 (Not Acceptable) → 406 Not Acceptable - 1660 (Conflict) → 409 Conflict - 1669 (Already Exists) → 409 Conflict - 1670 (Restricted) → 406 Not Acceptable - 1680 (Access Denied) → 401 Unauthorized - 1671 (Rate Limited) → 429 Too Many Requests - 1677 (Locked) → 423 Locked Billing errors: - 1688 (Subscription Required) → 402 Payment Required (org has no active subscription or free-tier credits exhausted) - 1695 (Upgrade Required) → 402 Payment Required (feature requires a higher-tier plan) - 1696 (Credit Limit Exceeded) → 402 Payment Required (free-tier credit limit exceeded; error message includes usage details) Server errors: - 1654 (Internal Error) → 500 - 1664 (Datastore Error) → 500 - 1686 (Not Implemented) → 501 - 1693 (Temporarily Unavailable) → 503 Service Unavailable — something the request needs is briefly busy or out of reach, so the request was not carried out at all. Unlike the client errors above it **is retryable, and a retry is the correct response**: wait a moment and send the same request again, unchanged. Do not rewrite the request, do not vary its inputs, and do not report it as a settled refusal. ### Credential scope errors Four codes report a credential that was verified and is too narrow for what it attempted. **All four are HTTP 403, never 401**, and on them `error.params` is an **object** (not the validation array) carrying `reason`, `entity_type`, `entity_id`, `required_access_mode`, `current_access_mode` and `credential_type`, plus `credential_id` and `credential_label` for an API-key caller. Branch on `params.reason`, not the number. - `10767` (`scope_admin_required`) — an administrative operation with a credential that is not admin-capable. Administrative **reads** count: org billing details, invoices, usage, credits, plan preview, payment method, and the events audit log all require an admin-capable credential. - `10768` (`scope_exceeds_issuer`, or `access_mode_exceeds_initiate` on an OAuth consent) — the requested scopes or access mode are broader than the credential making the request, or broader than the authorization was initiated with. - `10769` (`userdetails_scope_required`) — an account-settings operation (changing `password` or `email_address`, 2FA enrolment and its verification, `invalidate-all`) without the `userdetails:*:rw` scope. - `10770` (`scope_write_required`) — a non-`GET` method on an account-anchored route, attempted by a credential that holds no write-capable grant anywhere. A `user:*:r` credential reads the whole account and writes nothing: no exempt route, `sign-out/` included, so such a client ends its session by discarding the credential locally or calling `POST /current/oauth/revoke/`. Full detail: [Auth reference](https://api.fast.io/current/llms/auth/). ### Retired per-field codes As of 2026-05-05, error codes `136957`, `249170`, `279705`, `295625` are retired. The equivalent field-level failures now surface inside `error.params[]` with `kind: 'invalid'` (and a per-field `code` and `message` describing the specific violation). Clients that previously switched on those exact integers should switch on `params[].name` + `params[].kind` instead. ## Rate Limiting Headers: `x-ve-limit-avail` (requests remaining), `x-ve-limit-max` (window cap), `x-ve-limit-expires` (Unix-time the window resets). When exceeded: HTTP 429 with error code 1671 (Rate Limited). Back off until `x-ve-limit-expires`. ## Pagination ### Offset Pagination (List Endpoints) Most list endpoints support offset-based pagination: - `limit`: 1-500 (default: 100) — number of items to return - `offset`: 0+ (default: 0) — number of items to skip - Response: `pagination.total`, `pagination.limit`, `pagination.offset`, `pagination.has_more` ### Keyset Pagination (Storage) The storage listing endpoint uses cursor-based (keyset) pagination. A page may contain fewer than `page_size` items even when `has_more` is `true` — rely on `has_more` / `next_cursor`, not page fullness, to decide whether to continue. **Storage listing** (`GET /current/workspace/{id}/storage/`, etc.): - `sort_by`: name | updated | created | type (default: name) - `sort_dir`: asc | desc (default: asc) - `page_size`: 100 | 250 | 500 (default: 100) - `cursor`: opaque string from previous response - Response: `pagination.has_more`, `pagination.next_cursor`, `pagination.page_size` ## Compact Responses (`output=`) Most list and detail endpoints accept an optional `output` query parameter that selects a response shape tuned to how much detail the caller actually needs. This is useful for agents and clients that want to minimize payload size and token usage. - **Syntax:** `?output=` or `?output=,` — comma-separated list of tokens. - **Detail-level tokens (mutually exclusive — pick at most one per request):** - `terse` — the smallest useful shape: identifiers, primary labels, and the handful of fields needed to navigate between resources (types, parent linkage, share/target references, a ready/not-ready flag for previews). Best for tree traversal, picker UIs, autocomplete, and any workflow that will follow up with a detail call only on user interest. - `standard` — `terse` plus the operational context most list and detail views actually render: timestamps, lifecycle flags (closed/archived/suspended/deleted), short descriptions, plan/status fields, creator/owner refs, member status, and short summaries. Recommended default for most agent list/detail workflows. - `full` — the complete resource shape; equivalent to omitting `?output=` entirely. Use when you need branding, capability matrices, permission blocks, long-form AI summaries, metadata, feature flag blocks, or other rarely-read detail. - **Modifier tokens (orthogonal — may be combined with any detail level):** - `markdown` — switches the response encoding from JSON to GitHub-flavored Markdown. Response `Content-Type` becomes `text/markdown; charset=UTF-8`. Works on every endpoint that returns a JSON envelope, including error responses — a 406 with `?output=...,markdown` renders the error envelope as markdown too. Arrays of same-shaped records become GFM pipe tables; associative maps become `- **key:** value` bullet lists; `error` envelopes are promoted to a leading `# Error` section. String values are made inert before they are emitted: a value that looks like HTML or spans multiple lines is wrapped in a code fence sized so the value cannot close it, and every other value is backslash-escaped CommonMark-style so that links, emphasis, headings and table breaks inside caller-supplied text (file names, memos, labels) do not activate. The escaping is lossless — un-escaping `\X` for ASCII punctuation returns the original bytes, so a value that reads `\[Example\]` in the raw body displays as `[Example]` — and values containing nothing markdown-active are emitted unchanged. This is markdown escaping, not HTML sanitization: consumers that render markdown as HTML MUST still sanitize the output. - **Default behavior:** When `output=` is absent, responses are `full` JSON and byte-for-byte unchanged from previous API versions — existing clients require no changes. - **Combining level tokens:** Specifying more than one detail level in the same request (e.g. `?output=terse,standard`) is an error and returns **HTTP 406**. Combine a level with modifiers only (e.g. `?output=standard,markdown`). - **Combining markdown with a level:** `?output=terse,markdown`, `?output=standard,markdown`, `?output=full,markdown`, and `?output=markdown` (defaulting to `full`) are all valid. Token order does not matter — `?output=markdown,terse` is equivalent to `?output=terse,markdown`. - **Markdown with validation errors:** Malformed combinations such as `?output=terse,standard,markdown` still render the HTTP 406 response body as markdown — the encoding flip happens before validation. - **Related (`?format=md`):** A small set of endpoints (imports) additionally support an endpoint-specific `?format=md` parameter that renders domain-shaped markdown (status tables). The generic `?output=markdown` modifier works on every envelope response and renders the JSON envelope as markdown; `?format=md` is specific to those endpoints and produces a different, purpose-built shape. - **Unknown tokens:** Silently ignored for forward compatibility. New tokens may be added without bumping the API version. - **Cumulative fields:** `standard` is a superset of `terse`, and `full` is a superset of `standard`. Nothing disappears as you move up a tier. - **Scope:** Applies transparently to nodes (files/folders/notes/links), events, users, workspaces, orgs, and shares wherever they appear — including when nested inside other resources. Per-category field lists for `terse` and `standard` are documented in the relevant category reference (storage, events, orgs, workspaces, shares, auth/users). ## ID Formats - **Profile IDs** (user, org, workspace, share, file share): 19-digit numeric string - **Node IDs** (files, folders, notes): OpaqueId — see canonical OpaqueId format below. Node IDs are fully opaque; clients MUST NOT parse a resource type from the id prefix. Files, folders, and notes are not distinguishable by their id. - **Upload IDs / Web Upload IDs / Quickshare tokens / Chat IDs / etc.**: OpaqueId — same canonical format as node IDs. - **Special folder aliases**: `"root"` for storage root, `"trash"` for trash folder. ### Canonical OpaqueId Format The API emits OpaqueIds in a 34-character hyphenated form: 29 alphanumeric characters split into 5 groups of 5 plus a final group of 4, separated by hyphens (e.g., `2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4`). Treat the entire string as opaque — do not parse a resource type from any part of it, including the leading character. - **API output is always the hyphenated form.** Persist this form on the client. - **API input accepts either form.** The router strips hyphens before validation, so a 29-char un-hyphenated id (`2ltsuq4mjacuv7pgc5ydlxnsjwee4`) routes identically to its hyphenated equivalent. - **Equality:** Compare ids by stripping hyphens and lowercasing both sides — do not assume the wire form is normalized across all clients. - **No mixing rule:** Do not invent your own hyphenation. If a client receives an id, persist it byte-for-byte and pass it back unchanged. **Custom names as identifiers:** Most endpoints that accept a profile ID also accept a custom name: | Profile Type | Custom Name | |-------------|-------------| | Workspace | Folder name (e.g., `my-project`) | | Share | URL name (e.g., `q4-financials`) | | Organization | Domain name (e.g., `acme`) | | User | Email address (e.g., `user@example.com`) | ## Field Constraints Profile fields (org, workspace, share) have validation rules enforced server-side. | Entity | Field | API Key | Min | Max | Regex | Required | Nullable | |--------|-------|---------|-----|-----|-------|----------|----------| | Org | domain | `domain` | 2 | 63 | `^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$` | Yes (create) | No | | Org | name | `name` | 3 | 100 | No control chars (`\p{Cc}`) | Yes | No | | Org | description | `description` | 10 | 1000 | No control chars (`\p{Cc}`) | No | Yes | | Workspace | folder_name | `folder_name` | 4 | 80 | `^[\p{L}\p{N}-]+$` (unicode letters, digits, hyphens) | Yes (create) | No | | Workspace | name | `name` | 2 | 100 | No control chars (`\p{Cc}`) | Yes | No | | Workspace | description | `description` | 10 | 1000 | No control chars (`\p{Cc}`) | No | Yes | | Share | custom_name | `custom_name` | 4 | 80 | `^[\p{L}\p{N}\-_]+$` (unicode letters, digits, hyphens, underscores) | No (auto-generated when omitted) | No | | Share | custom_url | `custom_url` | 10 | 100 | — | Yes | Yes | | Share | title | `title` | 2 | 80 | No control chars (`\p{Cc}`) | Yes | Yes | | Share | description | `description` | 10 | 500 | No control chars (`\p{Cc}`) | No | Yes | **Notes:** - "No control chars" means the field rejects Unicode control characters (category `\p{Cc}`); other `\p{C}` subcategories such as format characters (`\p{Cf}`: bidi marks, ZWJ, etc.) are allowed - Org domain must be lowercase alphanumeric with optional hyphens, cannot start/end with hyphen - Workspace folder_name allows Unicode letters, digits, and hyphens - Share custom_name allows Unicode letters, digits, hyphens, and underscores; it is optional on create — when omitted, the server auto-generates a random URL name - Description max length for shares is 500, not 1000 like org/workspace ## Agent Workflows ### Upload Files Small files (< 4MB): single-request upload. Large files: chunked upload with parallel chunks. → Full details: [Upload reference](https://api.fast.io/current/llms/upload/) ### Query Documents with AI Create chats with `chat` (general) or `chat_with_files` (RAG with citations). Stream responses via SSE. Use `/storage/search` for both keyword and semantic search — when workspace intelligence is enabled, results automatically include ranked text snippets with relevance scores. Add `details=true` to include the full node resource (previews, AI state, metadata, size) for each result; default limit drops to 10 when details enabled. Search is better for retrieval/lookup; chat is better for synthesis/analysis. Choose what a search matches with the optional `search_in=filename|content|both` (default `both`). `filename` is the predictable, `find`-style surface: pair it with `name_match=exact|prefix|contains|glob` (default `auto`) and `case_sensitive` to match whole filenames — `name_match=glob` with `Quarterly*.pdf` finds `Quarterly Report.pdf`, spaces and all. `content` matches the AI's understanding of a file — its AI-generated summary and its meaning-based index — and is **not** a text scan of the file's bytes. Those are two separate channels, and intelligence gates only the meaning-based one: summaries indexed earlier stay searchable after AI features are switched off, so `content` can still return hits there. Read `search_metadata.semantic_available` for what a given request actually got. Omit all three parameters and behavior is exactly as before. → Full details: [AI reference](https://api.fast.io/current/llms/ai/) ### Share Files with Humans Create Send/Receive/Exchange shares with Portal or Shared Folder storage modes. → Full details: [Shares reference](https://api.fast.io/current/llms/shares/) ### Monitor Usage `GET /current/org/{org_id}/billing/usage/limits/credits/` — credit consumption `GET /current/org/{org_id}/billing/usage/meters/list/` — detailed breakdown `GET /current/events/search/` — activity feed → Full details: [Events reference](https://api.fast.io/current/llms/events/) ### Activity Polling Long-poll for changes instead of looping on individual endpoints: `GET /current/activity/poll/{entityId}?wait=95&lastactivity={timestamp}` → Full details: [Events reference](https://api.fast.io/current/llms/events/) ## Endpoint Summary ### System - `GET /current/ping/` — Health check (no auth) - `GET /current/system/status/` — System status (no auth) - `GET /current/llms/` — This reference file (no auth) - `GET /current/agents/` — Agent integration guide (no auth) - `GET /.well-known/oauth-authorization-server/` — OAuth authorization server metadata (RFC 8414, no auth) - `GET /.well-known/oauth-protected-resource/` — OAuth protected resource metadata (RFC 9728, no auth) ### Users & Auth → [Auth reference](https://api.fast.io/current/llms/auth/) User CRUD, authentication (Basic Auth, API keys, 2FA), email validation, password reset, user search. - `GET /current/auth/scopes/` — Token scope introspection. Returns auth_type (jwt_v2, jwt_v1, api_key, api_key_scoped), scopes array, scopes_detail (hydrated with entity names/domains/parents, each entry carrying `admin`), is_agent, agent_name, full_access, `admin` and `legacy`. `full_access` means account-wide **and may write** (`user:*:rw` or `user:*:rwa`) — `user:*:r` is `false`; `admin` means the credential can administer (a browser login session, or any `rwa` scope); `legacy` means the credential declares no scopes claim at all, which includes every browser login session. For scoped API keys, returns `auth_type: "api_key_scoped"` with hydrated scope details, agent name, and expiration. Requires auth. - `GET /current/user/auth/` — Basic Auth → JWT exchange. Optional `x-ve-session-cookie` request header additionally delivers the token in an HttpOnly browser session cookie - `POST /current/user/auth/bootstrap/` — Browser only: returns the session token held in the HttpOnly cookie, as-is (the one endpoint that authenticates from a cookie; POST-only, same-origin, no `Authorization` header). Call it once per page load to bring the token into memory - `POST /current/user/` — Create user account - `POST /current/user/update/` — Update user profile - `GET /current/user/{user_id}/details/` — User details - `POST /current/user/auth/key/` — Create API key - `GET /current/user/auth/keys/` — List API keys - `POST /current/user/auth/2factor/auth/{token}/` — 2FA verification (the limited-scope token is a required path segment) - `POST /current/user/email/validate/` — Email verification - `POST /current/user/email/reset/` — Password reset request - `GET /current/users/search/` — Search users - User Apps: `GET /current/user/apps/` (list installed apps), `POST /current/user/apps/install/`, `POST /current/user/apps/uninstall/`, `POST /current/user/apps/heartbeat/` — manage the caller's installed apps ### OAuth 2.0 → [OAuth reference](https://api.fast.io/current/llms/oauth/) PKCE authorization flow, token exchange/refresh, session management, Dynamic Client Registration (RFC 7591/7592), Client ID Metadata Document (CIMD), Resource Indicators (RFC 8707), scoped access tokens (JWT v2.0). - `GET /.well-known/oauth-authorization-server/` — Authorization server metadata (RFC 8414, no auth) - `GET /.well-known/oauth-protected-resource/` — Protected resource metadata (RFC 9728, no auth) - `POST /current/oauth/register/` — Dynamic client registration (no auth, rate limited) - `PUT /current/oauth/register/` — Update client registration (localhost-only clients) - `POST /current/oauth/token/` — Token exchange (authorization_code, refresh_token) - `POST /current/oauth/revoke/` — Revoke token - `GET /current/oauth/sessions/` — List OAuth sessions - `GET /current/oauth/authorize/info/` — Authorization request info ### Organizations → [Organizations reference](https://api.fast.io/current/llms/orgs/) Org CRUD, member management, billing/subscriptions, org discovery. - `POST /current/org/create/` — Create organization - `GET /current/org/{org_id}/details/` — Org details - `POST /current/org/{org_id}/update/` — Update org - `GET /current/org/{org_id}/members/list/` — List members - `POST /current/org/{org_id}/create/workspace/` — Create workspace - `GET /current/org/{org_id}/billing/details/` — Billing info - `GET /current/org/{org_id}/billing/invoices/` — List invoices with hosted payment links An Enterprise-plan org can also restrict which members may invite outside people onto its shares, portals and workspaces via three policy-envelope keys on org update/details (`external_invites_shares` / `_portals` / `_workspaces`) — see *Collaboration Policies* in the Organizations reference for the envelope shape and the two refusal reasons (`external_invites_denied`, `external_invites_object_denied`). An Enterprise-plan org can also cap what API keys and OAuth grants issued inside it may hold, checked again on **every later request**, not only when a credential is minted — see *Credential Policy* in the Organizations reference and *Org Credential Policy* in the Auth reference for the `credential_policy` setting and its three refusal reasons (`credential_policy_mode`, `credential_policy_scope`, `credential_policy_sso`). An Enterprise-plan org can also restrict whether cloud sync runs at all, and whether it may write local changes back to the connected provider — org `cloud_sync` (`{enabled, mode}`), met at runtime with each workspace's own `cloud_sync_mode` ceiling. See *Cloud Sync Policy* in the Organizations reference and *Cloud Sync Policy* in the Workspaces reference for the resolution order, the `effective_cloud_sync` / `effective_access_mode` fields, and the two refusal reasons (`cloud_sync_disabled`, `cloud_sync_read_only`). An Enterprise-plan org can also require a second factor at sign-in — org `auth_require_2fa`, a policy envelope of `{admin, member, overrides}` whose every value is `required` or `optional`, not a single scalar. It governs password and social login only, not API keys, OAuth or MCP tokens, and an SSO-minted session is always compliant. `GET /current/user/auth/` and the social sign-in callback both gain an always-present `enrol_required` boolean; when `true`, the issued token is a narrow enrolment credential rather than a session. See *Require-2FA Policy* in the Organizations reference and *Interactive Login & Enrolment* in the Auth reference. ### Workspaces → [Workspaces reference](https://api.fast.io/current/llms/workspaces/) Workspace CRUD, member management, assets, intelligence setting, discovery. - `GET /current/workspace/{workspace_id}/details/` — Workspace details - `POST /current/workspace/{workspace_id}/update/` — Update workspace - `GET /current/workspace/{workspace_id}/members/list/` — List members - `POST /current/workspace/{workspace_id}/create/share/` — Create share - `GET /current/workspace/{workspace_id}/list/shares/` — List shares ### Storage → [Storage reference](https://api.fast.io/current/llms/storage/) File/folder CRUD (both workspace and share), locking, previews, transforms, download tokens, recently modified files. - `GET /current/workspace/{id}/storage/{parent}/list/` — List folder contents - `GET /current/workspace/{id}/storage/{node}/details/` — Node details - `GET /current/workspace/{id}/storage/{node}/read/` — Download file - `POST /current/workspace/{id}/storage/{parent}/addfile/` — Add uploaded file - `POST /current/workspace/{id}/storage/{parent}/createfolder/` — Create folder - `POST /current/workspace/{id}/storage/{node}/lock/` — Acquire file lock - `POST /current/workspace/{id}/storage/{node}/lock/heartbeat/` — Renew lock - `DELETE /current/workspace/{id}/storage/{node}/lock/` — Release lock - `POST /current/workspace/{id}/storage/{node}/lock/override/` — Take over a lock held by someone else (write access required); returns YOUR new `lock_token`. Takes the lock only — a stale `if_version_id` still conflicts; this is not "force save" - `GET /current/workspace/{id}/storage/search/` — Search files. Optional `search_in` (`filename`/`content`/`both`), `name_match` (`auto`/`exact`/`prefix`/`contains`/`glob`), and `case_sensitive` - `GET /current/workspace/{id}/metadata/search/` — Search nodes by metadata field values; each result names the field(s) that matched (workspace-only) - `GET /current/workspace/{id}/search/` — Unified search: files, metadata, and comments in one call, grouped into per-type buckets with independent pagination. The same `search_in` / `name_match` / `case_sensitive` parameters shape the `files` bucket - `GET /current/share/{id}/search/` — Unified search across a share (files and comments buckets) ### Shares → [Shares reference](https://api.fast.io/current/llms/shares/) Share CRUD, types (Send/Receive/Exchange), storage modes (Portal/Shared Folder), members, branding, durable File Share (single-file links), anonymous file drop (guest auth). Shares support `download_security` levels: `off` (default), `medium` (preview-only for guests via nonce flow), `high` (downloads disabled for guests). - `POST /current/workspace/{id}/create/share/` — Create share - `GET /current/share/{share_id}/details/` — Share details - `POST /current/share/{share_id}/update/` — Update share - `GET /current/share/{share_id}/members/list/` — List members - `POST /current/share/{share_id}/auth/guest/` — Guest authentication - `POST /current/share/{share_id}/auth/password/` — Password authentication ### File Share (durable single-file links) → [Shares reference](https://api.fast.io/current/llms/shares/) A **File Share** is a durable, link-shareable view of ONE file from a workspace. It is the successor to the deprecated QuickShare: it is durable by default (an optional expiry can be set), and access is governed by three tiers (`anyone_with_link`, `any_registered`, `named_people`) plus optional per-user grants (`view` / `download` / `edit`) and an optional link password (passed via the `x-ve-password` request header, never the URL). A holder of an `edit` grant can replace the shared file's content by targeting the File Share id from an upload session. - `POST /current/workspace/{id}/create/fileshare/` — Create a File Share bound to a file node - `GET /current/workspace/{id}/list/fileshares/` — List a workspace's File Shares - `GET /current/fileshare/{fileshare_id}/details/` — Public viewer metadata - `GET /current/fileshare/{fileshare_id}/storage/read/` — Download the bound file - `GET /current/fileshare/{fileshare_id}/storage/preview/{preview_type}/read/` — Preview the bound file - `GET /current/fileshare/{fileshare_id}/storage/versions/` — List the bound file's version history - `POST|PATCH /current/fileshare/{fileshare_id}/update/` — Update title / access tier / password - `GET|POST|DELETE /current/fileshare/{fileshare_id}/grants/` — List, grant, or revoke per-user capabilities - `DELETE /current/fileshare/{fileshare_id}/delete/` — Delete the File Share - `GET /current/fileshare/{fileshare_id}/storage/readnote/{note_id}/` — Read a shared note's content; `POST /current/fileshare/{fileshare_id}/storage/updatenote/{note_id}/` — Update a shared note (requires an edit grant) - `GET /current/fileshare/{fileshare_id}/realtime/note-auth/{note_id}/` — Mint a note-scoped realtime token for collaborative editing of the shared note > **QuickShare is deprecated.** Creating or extending a QuickShare (`POST /current/workspace/{id}/storage/{node}/quickshare/`) now returns **403** (`10756 (Quickshare Deprecated)`) with a directed message; create a durable File Share instead. Existing QuickShare links can still be viewed, downloaded, and revoked during the drain. ### Upload → [Upload reference](https://api.fast.io/current/llms/upload/) Chunked upload flow, web upload (URL import), session management. - `POST /current/upload/` — Create upload session - `POST /current/upload/{id}/chunk/` — Upload chunk - `POST /current/upload/{id}/complete/` — Finalize upload - `GET /current/upload/{id}/details/` — Check upload status - `POST /current/web_upload/` — Import file from URL ### AI & Agent (formerly Chat) → [AI reference](https://api.fast.io/current/llms/ai/) **v3.5 path rename:** the AI agent endpoints moved from `/ai/chat/` to `/ai/agent/`. The legacy `/ai/chat/` paths have been **retired and removed** — they no longer respond, and `/ai/agent/` is now the only path. If you have an older integration still calling `/ai/chat/...`, update it to the equivalent `/ai/agent/...` path. The core request and response shapes carry over, but **file attachment changed**: attach a file or folder by including it as a reference item (`{type, id}`, files may add `file_details.version_id`) in the `references` / `content_parts` / `subjects` arrays — the retired `files_attach` / `files_scope` / `folders_scope` string params are gone, and a file that can't be attached now returns an error instead of being silently ignored. See the AI reference for the full migration note. RAG chat, general chat, semantic search, notes, saved metadata filters, AI extraction, SSE streaming. Saved metadata filters are available on all plan tiers, with per-filter node caps that scale by plan (lower tiers have small caps; a filter's node listing returns a `scope` object naming every way the answer was bounded, so a short list is never mistaken for a complete one). Two plan features gate AI: `content_ai` (master switch for all AI surfaces) and `ai_agent` (interactive agentic flows — create chat, send message, enable the `intelligence` indexing toggle). All current plans (Starter, Business, Growth) include both. See [AI reference: Plan Requirements](https://api.fast.io/current/llms/ai/#plan-requirements) for the matrix. - `POST /current/workspace/{id}/ai/agent/` — Create AI chat - `POST /current/workspace/{id}/ai/agent/{chat_id}/message/` — Send message - `GET /current/workspace/{id}/ai/agent/{chat_id}/message/{msg_id}/read/` — Stream response (SSE) - `GET /current/workspace/{id}/metadata/eligible/` — List nodes (files and notes) eligible for metadata extraction, each with its extracted metadata values inline - `GET /current/workspace/{id}/metadata/fields/` — List the workspace's metadata field vocabulary (names, types, constraints, aliases, `file_count`) - `POST /current/workspace/{id}/metadata/fields/merge/` — Fold one metadata field into another, retiring the source's name into the target's aliases (workspace admin; irreversible). **`confirm` is a BOOLEAN here, not the resource-name string it is on delete-style endpoints** — omit it, or send `false`, for a pre-flight that writes nothing - `POST /current/workspace/{id}/storage/{node}/metadata/extract/` — Enqueue async metadata extraction (returns HTTP 202 + job_id; poll `/jobs/status/`) - `POST /current/workspace/{id}/storage/{node}/metadata/extract-all/` — Folder-level batch metadata extraction across a folder's eligible nodes (`root` for the whole workspace; optional `fields` scopes the run to named fields) - `GET /current/workspace/{id}/metadata/filters/` — List the workspace's saved metadata filters - `POST /current/workspace/{id}/metadata/filters/` — Create a saved metadata filter - `GET /current/workspace/{id}/metadata/filters/{filter_id}/` — Saved filter details - `PUT /current/workspace/{id}/metadata/filters/{filter_id}/` — Update a saved filter - `DELETE /current/workspace/{id}/metadata/filters/{filter_id}/` — Delete a saved filter - `GET /current/workspace/{id}/metadata/filters/{filter_id}/nodes/` — Run a saved filter and list the nodes it matches - `POST /current/workspace/{id}/metadata/compound-search/` — Compound search: a metadata `filters` predicate intersected with a semantic `content_query`, in one call. Workspace-only; needs Member, the `metadata` **and** `content_ai` plan features, and Intelligence enabled on the workspace. Returns `items` plus a mandatory `scope` object naming every way the answer was bounded. See *Compound Search* in the [Storage reference](https://api.fast.io/current/llms/storage/) **Metadata templates have been retired and removed.** Every `/metadata/templates/...`, `/metadata/template/...`, `/storage/{node}/metadata/template_select/` and `/storage/{node}/metadata/templates/` path is finished. The workspace-scoped ones were deleted, so they no longer route and answer `9992` — indistinguishable from a mistyped path. The two node-scoped ones still have a handler and answer `410 Gone` — `error.code` `122209` for `/storage/{node}/metadata/template_select/` and `131380` for `/storage/{node}/metadata/templates/` — which says explicitly that the path will not return. Neither is worth a retry. Structured metadata is now organised by **saved metadata filters** — a stored predicate over the workspace's field vocabulary plus an optional ordered projection — listed above. A filter's membership is computed from its predicate, so files are no longer added to or removed from a set by hand, and there is no folder-level assignment. Field names, types and constraints come from the workspace field vocabulary (`/metadata/fields/`), which is discovered from the metadata that extraction and metadata writes produce rather than declared up front. ### How-To → [How-To reference](https://api.fast.io/current/llms/howto/) Single-call natural-language product help: ask a "how do I…" question about Fastio and get a grounded answer (or a clarifying question) back. Top-level endpoint; **free** — any authenticated user may call it, no org required, no entity is ever charged (cached or live), bounded only by a per-user rate limit. - `POST /current/how-to/` — Ask a how-to question (`question` required ≤2000 chars; optional `context` ≤8000 chars). Returns HTTP 200 with `status:"answer"` (`answer`, `escalated`, `topics_used`) or `status:"needs_clarification"` (`questions`). ### Events & Activity → [Events reference](https://api.fast.io/current/llms/events/) Event search/filtering, activity polling, WebSocket realtime. - `GET /current/events/search/` — Search events - `GET /current/events/search/summarize/` — AI event summary - `GET /current/event/{event_id}/details/` — Event details - `POST /current/event/{event_id}/ack/` — Acknowledge event - `GET /current/activity/poll/{profile_id}/` — Long-poll for changes - `GET /current/realtime/auth/validate/` — Validate realtime token - `GET /current/realtime/note-auth/{profile_id}/{note_id}` — Mint a note-scoped realtime token for collaborative note editing (900s TTL) - `GET /current/realtime/note-auth/validate/` — Validate a note-scoped realtime token - `GET /current/websocket/auth/{profile_id}` — WebSocket auth ### Comments → [Comments reference](https://api.fast.io/current/llms/comments/) Comment CRUD (JSON body), threading, mentions, reactions, reference anchoring. - `POST /current/comments/{entity_type}/{parent_id}/` — Create comment - `GET /current/comments/{entity_type}/{parent_id}/` — List comments - `GET /current/comments/{comment_id}/details/` — Comment details - `POST /current/comments/{comment_id}/update/` — Edit comment by ID - `POST /current/comments/{comment_id}/reactions/` — Add reaction - `DELETE /current/comments/{comment_id}/delete/` — Delete comment ### Signing / E-Signature → [Signing reference](https://api.fast.io/current/llms/signing/) Audit-archive SignEnvelopes holding up to 20 PDFs sent to recipients for electronic signature. Internal PAdES-LT cryptographic signing with downloadable audit certificate. Two surfaces: sender/admin (bearer JWT) and signer (path-token JWT). The org's billing plan gates the whole surface (exposed as `capabilities.signing`). **Sender / Admin (workspace-parented):** - `POST /current/workspace/{workspace_id}/sign_envelopes/create/` — Create a draft envelope - `GET /current/workspace/{workspace_id}/sign_envelopes/list/` — List envelopes - `GET /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/details/` — Get envelope - `POST|PATCH /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/update/` — Update draft - `POST /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/send/` — Send (Draft → Sent) - `POST /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/void/` — Void (reason required) - `GET /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/download/` — Stream the source PDF bytes (application/pdf; Bearer-authed) - `GET /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/preview/` — Stream the source PDF for inline preview (application/pdf) - `GET /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/signed/download/` — Stream the signed PDF bytes (application/pdf; 404 until the document completes) - `GET /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/audit/download/` — Stream the audit certificate (JSON evidence record; 404 until the envelope is terminal) - `GET /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/audit/pdf/download/` — Stream the audit certificate as a rendered PDF (application/pdf; 404 until the envelope is terminal) - `POST /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/my_sign_link/` — Mint the caller's own signing link for the envelope; returns actionable/blocked/terminal/reauth decision (requires write-scope token) **Signer surface (public, path-token JWT):** - `GET /current/sign_envelopes/signer/{token}/view/` — Landing (envelope state, documents, fields, consent) - `GET|POST /current/sign_envelopes/signer/{token}/authenticate/` — Issue or verify OTP - `POST /current/sign_envelopes/signer/{token}/sign/` — Submit consent + field values (queues async signing) - `GET /current/sign_envelopes/signer/{token}/status/` — Poll signing pipeline (adaptive `next_poll_seconds`) - `POST /current/sign_envelopes/signer/{token}/decline/` — Decline (cascades envelope to Declined) **Provider webhook (public, provider HMAC):** - `POST /current/webhooks/sign_envelopes/{provider}/{connection_short_id}/` — Inbound provider event (Documenso supported) **Plan availability:** Signing availability depends on your organization's plan. See the [Signing reference](https://api.fast.io/current/llms/signing/) for details. Lifecycle: `draft` → `sent` → `in_progress` → `completed` | `declined` | `voided` | `expired` | `failed`. Activity events: `sign_envelope_drafted`, `sign_envelope_sent`, `sign_envelope_voided`, `sign_envelope_viewed`, `sign_envelope_recipient_signed`, `sign_envelope_recipient_declined`, `sign_envelope_document_signed`, `sign_envelope_completed`, `sign_envelope_expired`. ### Dashboard → [Dashboard reference](https://api.fast.io/current/llms/dashboard/) Per-workspace, per-member feed of ranked, paginated actionable cards. Cards are drawn from @mentions, file activity, and pending signatures. When the workspace plan includes AI features, an AI overlay adds urgency scores (0–100), summaries, and suggested actions to each card; cross-item synthesis cards appear at the end. Each card carries a `ripley_seed` for pre-focused Ripley Agent conversations. Dismiss/snooze is per-member and out-of-band (never advances the underlying item). - `GET /current/workspace/{workspace_id}/dashboard/` — Get ranked, paginated card feed (`limit` 1–200, default 50; response: `cards`, `pagination`, `dismissed_recent_count`) - `POST /current/workspace/{workspace_id}/dashboard/cards/{card_key}/dismiss/` — Dismiss or snooze a card (`snooze_until` optional; URL-encode `card_key` which contains `:`) - `DELETE /current/workspace/{workspace_id}/dashboard/cards/{card_key}/dismiss/` — Undismiss (restore card to feed) - `POST /current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/my_sign_link/` — Mint signing link for a dashboard signature card; returns actionable/blocked/terminal/reauth decision (requires write-scope token) ### Agent Intents → [Agent Intents reference](https://api.fast.io/current/llms/intents/) Workspace-scoped, short-lived declarations of what an agent is doing, so peers see a collision before it happens rather than after. Allocation is content-free (an unfilled slot is occupancy, not an incomplete write); filling a slot is the only heartbeat (no separate renewal call) and is a compare-and-set on `version`. Slot identity is credential-keyed — `(workspace, node, user, agent_name)`, so five agents of one operator hold five slots — but **ownership is scoped to the user**: any credential authenticated as the holder may fill or release a slot, so sibling agents of one operator can pick up each other's, while another user's slot answers `404`. Rows carry `node` beside `node_id` and `locker` beside `locker_uid` — each resolved at read time and legitimately `null`. Workspace Member permission required; workspace-only, no share variant. - `POST /current/workspace/{workspace_id}/intents/` — Allocate a slot (no `topic`/`message` — content-free by design) - `GET /current/workspace/{workspace_id}/intents/` — Browse the workspace's live intents, topics only, ordered by allocation (`cursor` for paging) - `POST /current/workspace/{workspace_id}/intents/{intent_id}/` — Fill or refine a slot (requires `version`; also pushes `expires_at` forward) - `GET /current/workspace/{workspace_id}/intents/{id1},{id2}/` — Expand one or more intents in one call (full `message` included) - `DELETE /current/workspace/{workspace_id}/intents/{intent_id}/` — Release a slot (idempotent) ### Cloud Sync (External Storage Sync) Cloud Sync connects external cloud storage providers to workspace storage. The provider set is `google_drive`, `dropbox`, `box` and `onedrive_business`, but **which of them a given workspace may actually connect varies — read the providers endpoint below rather than assuming any particular one is on offer.** On opt-in read-write sources, local edits/adds/deletes are pushed back to the provider (two-way sync). Requires `cloud_import` feature enabled on the workspace. Identities are **per-user**: each member connects their own account (owned by them, up to 4 active per member per workspace); sensitive identity fields are masked for non-owners. **Feature Toggle (workspace admin):** - `POST /current/workspace/{workspace_id}/cloud-import/enable/` - Enable cloud sync - `POST /current/workspace/{workspace_id}/cloud-import/disable/` - Disable cloud sync **Available Providers:** - `GET /current/cloudsync/workspace/{workspace_id}/providers/` - List providers available on workspace's plan **Provider Identities:** - `GET /current/cloudsync/workspace/{workspace_id}/identities/` - List provider identities - `POST /current/cloudsync/workspace/{workspace_id}/identities/provision/` - Provision your own identity. **Every provider** returns an `authorize_url` for the browser OAuth-connect flow: the owner consents in a browser and the connection is finished by the completion endpoint below, so polling alone never activates them, and there is no address to share a folder with — the connected account IS the access. **Provisioning requires workspace Member on every provider** — a Viewer is refused, because the grant is only usable by someone who can create a source. Which providers a workspace may connect at all depends on more than one thing — the plan is only part of it, and a provider can be unavailable to a workspace whose plan grants it — so read the providers endpoint above rather than assuming all four. **Share with a level that can WRITE if the source will be `read_write`** (for Box, `Editor` rather than `Viewer`): nothing verifies the level you granted at connect time — the provider is the only thing that ever checks it, and it does so at transfer time — so a read-only grant connects and syncs happily and then fails the first write-back permanently. - `GET /current/cloudsync/workspace/{workspace_id}/identities/{identity_id}/` - Identity details - `POST /current/cloudsync/workspace/{workspace_id}/identities/{identity_id}/revoke/` - Revoke identity (owner or admin) - `POST /current/cloudsync/oauth/{provider}/complete/` - Finish a browser OAuth connect (`provider` is `dropbox`, `onedrive`, `box` or `google_drive`). The provider redirects the browser to a route on the **app** origin (`https://go.{host}/imports/oauth/{provider}/callback`); that route posts what it was handed here, on the user's session. Body: `{"code": "…", "state": "…"}`, or `{"error": "…", "state": "…"}` when the user declines — **post the decline too**, or the identity is left `provisioning` with nothing coming to finish it. Requires the caller to BE the user who started the connection: anyone else gets `1680 (Access Denied)`, which is what stops a forwarded `authorize_url` from attaching somebody else's cloud account to your identity. Returns the `identity`, `connected`, and `connect_error_code` (`null` on success; otherwise `declined`, `exchange_failed`, `credential_store_failed`, `activation_failed`, or — OneDrive only — `insufficient_scope`, which also carries an `admin_consent_url`). A `1609 (Not Found)` here is settled — the identity is gone and the flow must start again. `1664 (Datastore Error)` means the identity could not be READ, and **the message says whether to retry**: "Failed to load the connection being completed." is raised before the authorization is redeemed and should be **posted again with the same `code` and `state`**, while "The connection could not be verified after authorization; start a new connection." is raised after it has been redeemed — the link is spent, so start again from `provision` rather than reposting. There is deliberately no public callback endpoint to call instead. **Drive Catalog (OneDrive for Business only):** A OneDrive identity is one connected Microsoft account — **work or school, or personal**. A work or school account can usually reach several document libraries — its own OneDrive, plus whatever SharePoint it has access to — with no meaningful default among them. A personal account has exactly one drive and no SharePoint at all. So which library an import uses is chosen **per source** and must be picked from the identity's own catalog. - `GET /current/cloudsync/workspace/{workspace_id}/identities/{identity_id}/drives/` - List the libraries this identity can reach (**identity owner only** — a workspace admin is refused, because this catalog is one person's own Microsoft content; admins keep every removal power). Reads stored rows only — it never contacts the provider. Optional `site_path` filters to one SharePoint site. Returns `drives[]` plus `drives_state`, `drives_refreshed_at`, `drives_error` and `requires_site_path`. - `POST /current/cloudsync/workspace/{workspace_id}/identities/{identity_id}/drives/refresh/` - Re-enumerate the catalog from the provider (**identity owner only**, same rule as the list endpoint). Asynchronous: it returns immediately with `drives_state: "refreshing"` and no job id — poll the drives endpoint until the state leaves `refreshing`. Optional `site_path` enumerates one named site and replaces only that site's rows, which is how an account whose sites cannot be enumerated builds a catalog one site at a time. One refresh at a time per identity; a second call while one is genuinely in flight is rejected. `drives_state` is the field to branch on, because an EMPTY list is the normal state on a first connection — nothing is enumerated until a refresh is asked for — so `drives: []` alone cannot say why: | `drives_state` | Meaning | What the caller does | |---|---|---| | `never_refreshed` | Nobody has enumerated this identity yet | Call the refresh endpoint | | `refreshing` | An enumeration is queued or running | Poll until it changes | | `ready` | The catalog holds at least one library | Let the user pick one | | `empty` | The enumeration completed and found none | The connected account reaches no libraries; check the account, or name a site | | `requires_site_path` | The enumeration could not prove it saw everything reachable, and found nothing to offer. **Not a permission denial** | Refresh again with `site_path` | | `permission_denied` | Something the enumeration NAMED was refused. A site path will not fix it | The account needs access, or reconnect with fuller consent | | `failed` | The enumeration itself failed (provider outage, token error) | Retry; `drives_error` carries the detail | `drives_error` is normally null on success, with ONE exception: a `ready` catalog that was cut off at an internal ceiling carries a truncation note. The libraries listed are real and selectable — the list is just incomplete — so present it as a warning beside a usable list, not as a failure. **A partial consent is not an error.** A user who grants access to their files but withholds the SharePoint permission connects successfully — the catalog simply covers their own OneDrive and no site libraries. That is a working connection with a smaller catalog, not a failed one. **Import Sources:** - `GET /current/cloudsync/workspace/{workspace_id}/sources/` - List import sources. Every source object carries `remote_folder_web_url`, a browser link to the folder at the provider, built from the folder's own id so it survives a rename. **Null means exactly one thing — no valid stored ID-to-URL mapping can be built**: no id is stored yet (a source acquires one on a sync, so an existing source can gain a link later without anything being done to it), the provider publishes none, the provider has no URL that addresses a folder by id (Dropbox and OneDrive for Business are always null; Google Drive and Box return a link), the stored id is not shaped like that provider's ids, or the connected account could not be read. Never build your own link out of `remote_path`: it breaks the moment the folder is renamed. Every source object also carries `conflict_count` — write-backs on that source awaiting a `resolve` decision. It was previously on the details response only, so telling which of a workspace's grafts needed attention meant opening each one; it is computed for the whole page in one grouped query, so reading the list is not more expensive per source than it was. - `POST /current/cloudsync/workspace/{workspace_id}/sources/discover/` - Discover remote folders (**the identity owner only, on every provider** — a non-owning workspace admin gets `1680`, because discovery browses that member's own cloud account. The same gate covers reading the discovery job's results). **OneDrive for Business requires `drive_id`** — the folders discovered are the ones inside that library. Optional `remote_path` enumerates the folders one level below that remote folder instead of the provider root (max 2048 characters); browse a tree by passing each folder back on the next call. Each entry carries `remote_id`, `name`, `remote_path`, `type`, `size` and `already_imported`. **`already_imported` follows the FOLDER, not the path**: TWO keys are checked and a match on EITHER sets it — `remote_id` against the folder id of a source **on the same drive**, or `remote_path` against a source's path (empty never matches empty on either). So a connected folder renamed or moved at the provider is still reported as already imported. The id key is drive-scoped because provider item ids are unique within a drive, not across them — an unscoped one would block you from importing a folder you never imported; the path key is unscoped, which is long-standing behaviour. - `POST /current/cloudsync/workspace/{workspace_id}/sources/create/` - Create import source (**only the owner of the connected cloud account can import its folders — a workspace admin cannot do it on someone else's behalf**, though an admin can still remove any user's import afterwards; that owner must additionally be a Member of the workspace, so **guests cannot create a source**: it opens a standing, unattended sync channel into workspace storage, so it gates above the level an ordinary one-off storage write needs); `access_mode` = `read_only` (default) or `read_write`. **OneDrive for Business requires `drive_id`**, taken from the drives endpoint above; it is validated against that identity's catalog, and the library's type and name are recorded server-side rather than accepted from the request. The other providers do not take it. Optional `destination_node_id` chooses the workspace folder the import lands under — it must be an existing FOLDER in this workspace, and one that is not already part of an import (rejected at create as `destination_nested`); omit it for the `Imports` system folder at the storage root. Read the outcome back as a **pair**: the source carries `destination_node_id` (what was chosen) and `destination_fallback_at` (set when that folder could not be used at graft time and the import landed in `Imports` instead) — the first alone reads as a successful placement even when the import actually fell back. Optional `remote_id` — the value from the chosen discovery entry — is **advisory** and never stored: it can only make the duplicate check refuse a folder it would otherwise have admitted, which is how a folder renamed or moved since it was connected gets recognised. The check ORs the id and path keys, both drive-scoped **on this endpoint** (discovery's path key is not, so the same path in two libraries can read as already imported there and still be accepted here), so a `remote_id` matching nothing cannot be used to slip a duplicate past the path comparison. A folder this workspace already has is refused with "This folder is already connected to this workspace"; the same folder on a **different drive** is a different folder and is allowed. **Source Details:** - `GET /current/cloudsync/details/{source_id}/` - Source details (adds `provider_name`, `owner_user_id`, `is_owner`; `conflict_count` is on the list response too, so a badge does not need this call) - `POST /current/cloudsync/details/{source_id}/update/` - Update settings incl. `access_mode` (owner or admin). `drive_id` is **create-time only** and is rejected here: the library a source syncs is part of what the source IS, and repointing it at another one would make every already-imported file's origin wrong. Create a second source instead. `destination_node_id` is rejected here for the same reason (`destination_immutable`) — move the imported folder in the workspace instead; the sync follows it. - `POST /current/cloudsync/details/{source_id}/delete/` - Soft-delete source (owner or admin) - `POST /current/cloudsync/details/{source_id}/disconnect/` - Disconnect (owner or admin). `action: "keep"` leaves the imported files as ordinary workspace content; `action: "delete"` moves them to the trash. **`delete` is refused with `403` when the imported folder holds anything this source did not import** — something a member added or moved in, an item belonging to another connected folder, or a structure too deeply nested or looping to check — and a refusal changes **nothing at all**: no file is trashed and the source stays connected, so it can be retried once the cause is cleared. `keep` is not a blanket-safe fallback: it releases every imported item in the folder, including one belonging to another connected folder. A cleanup that cannot be proved to have happened is an error, never a `200` — `data_deleted` in the success body is `null` for `keep` and always `true` for `delete` - `POST /current/cloudsync/details/{source_id}/refresh/` - Trigger immediate sync (owner-or-admin) **Import Jobs:** - `GET /current/cloudsync/details/{source_id}/jobs/` - List sync jobs - `GET /current/cloudsync/details/{source_id}/jobs/{job_id}/` - Job details with progress - `POST /current/cloudsync/details/{source_id}/jobs/{job_id}/cancel/` - Cancel job (workspace admin) **Write-back (Read-Write Sources):** reading the queue is member-level, but every write operation requires the **member who owns the source's identity, or a workspace admin** — the transfer runs under that owner's cloud credential, so a push, a retry, or a `keep_local` resolve writes into their personal cloud account. A member below that level gets `1680 (Access Denied)`. Each write operation also has a **state precondition**, and a request that violates one is answered `1660 (Conflict)` → **409**, never a 5xx: `retry` needs a job in `failed`, `resolve` needs one in `conflict`, `cancel` needs one in `pending` or `conflict`, and `push` needs the source's `access_mode` to be `read_write` **and a `{node_id}` naming an imported FILE or NOTE**. That last one is worth spelling out: a folder inside a synced folder is an imported node just as much as a file is, so a folder's id is a plausible thing to send — it is refused at request time with the same `1660 (Conflict)` → **409**, rather than accepted with a `200` and a queued job that can only fail later. A state-precondition refusal is a settled client-side condition, not a fault to wait out — **resending the same request unchanged can never succeed**. Re-read the job (or the source) and act on the state it is actually in. **The other refusals on these endpoints are the opposite — transient — and must not be read the same way.** `push`, `retry`, `resolve` and `cancel` each take a short exclusive turn on the node they act on, so two write-back actions for one file cannot interleave, and **three distinct things can go wrong with that turn — each answering `1693 (Temporarily Unavailable)` → 503, and on each of them nothing was written.** (1) The turn could not be **taken**: "Another write-back action for this node is in progress" — another action is holding it right now; all four endpoints. (2) The turn was taken but the already-covered check **could not be carried out**: "Could not check whether this node already has a write-back in progress" — the request was never evaluated on its merits; `push`, `retry` and `resolve` (`cancel` has no such check, because it removes a live job rather than creating one, so coverage is irrelevant to it). (3) The turn was taken and **held, then lapsed before the change was applied**: "The write-back lock for this node expired before the change was applied" — the turn is short-lived and is re-proved immediately before the write, it had expired by then, and the request stopped there; all four endpoints. These are three different facts, not three phrasings of one — *could not start*, *started but could not look*, *started and lost the turn before writing* — and only the third got as far as being ready to write, which is exactly why it is worth saying that it still wrote nothing: re-sending it cannot queue, re-queue or cancel anything twice. Back off briefly and send the same request again, unchanged; these are the write-back outcomes a plain retry fixes. Separately, `push`, `retry` and `resolve` refuse a node already covered by **another** live write-back — one sitting in `pending`, `uploading` or `conflict`, for the same `operation` — with `1660 (Conflict)` → **409** and the message "This node already has a write-back in progress". **That 409 is NOT settled**: it clears on its own once the covering job finishes. Re-read the queue and wait for that job to reach a terminal status; do not repost in a tight loop, and do not treat it as permanent. On `resolve` the check **excludes the job being resolved** — a `conflict` job is itself live, so counting it would refuse every resolve — meaning a resolve is never blocked by its own row, only by a *different* live write-back that appeared on the same node while the conflict waited for a decision. On `retry` and `resolve`, tell the two 409s apart by re-reading the job — still in the state the action requires means the refusal was the transient one, any other state means it was the settled one. On `push` there is no job id to re-read, so the refusal itself is the discriminator: an access-mode or not-an-imported-file-or-note refusal is settled, and only the already-covered one clears as the queue drains. On `cancel` there is only one 409, the state precondition, and it is settled. In short: **`1693` → 503, retry it; a `1660` saying the node is already covered, re-read and wait; a `1660` on a state precondition, settled — never retry.** - `GET /current/cloudsync/details/{source_id}/writebacks/` - List write-back jobs (any member) - `POST /current/cloudsync/details/{source_id}/writebacks/push/{node_id}/` - Force-push one imported file or note (owner or admin; a folder's id is refused `409` with `error.code` `166884`, and a node another live write-back already covers gets a transient `409` with `error.code` `104972` — wait rather than repost) - `GET /current/cloudsync/details/{source_id}/writebacks/{writeback_id}/` - Write-back details (any member) - `POST /current/cloudsync/details/{source_id}/writebacks/{writeback_id}/retry/` - Retry a failed job (owner or admin; also answers `1660 (Conflict)` when another live write-back already covers the node — that one is transient, so wait rather than repost) - `POST /current/cloudsync/details/{source_id}/writebacks/{writeback_id}/resolve/` - Resolve a conflict (`resolution`: `keep_local` force-push, or `keep_remote` pull remote over local) (owner or admin; also answers the transient `1660 (Conflict)` when a *different* live write-back already covers the node — the check excludes the job being resolved, so a conflict is never blocked by its own row) - `POST /current/cloudsync/details/{source_id}/writebacks/{writeback_id}/cancel/` - Cancel a pending/conflict job (owner or admin) Providers: `google_drive`, `dropbox`, `box`, `onedrive_business` (which of these a workspace may connect varies — read the providers endpoint). **All four** connect a real per-user account through a browser OAuth consent, so each identity carries that account's own address. Google Drive and Box previously used centrally held service credentials provisioned in the background; that model was removed — it could only ever reach content someone had shared with a robot in Fastio's own tenant. Source statuses: pending, discovering, syncing, synced, error, paused, disconnect_pending, disconnecting, disconnected, suspended_plan (the workspace's plan no longer includes cloud sync; files and graft are kept, only scheduling stops, and the source resumes by itself once the plan covers cloud sync again), suspended_policy (the org or workspace cloud-sync policy has `enabled: false`; same "nothing to repair, resumes on its own" behavior as `suspended_plan` — see *Cloud Sync Policy* in `llms/workspaces.txt`). A `mode: "read"` policy changes no source status — it stops only outbound write-back, and a queued push under it is deferred (never failed), retiring at a bounded ~5-day ceiling with `properties.terminal_reason: "policy_hold_expired"` if the policy never widens. Every source object also carries `effective_access_mode` / `effective_access_mode_reason` (the source's own `read_only`/`read_write` vocabulary, composed from the acting caller's cloud-sync policy; `null` means undetermined, never permissive). Write-back is per-source opt-in and conflict-guarded (remote-mtime baseline at job creation; deletes fail closed if the remote mtime is unavailable); `keep_local` force-pushes, `keep_remote` pulls remote over local. The import graft folder is movable/renamable but cannot be nested inside another import folder; trashing it disconnects the source. Imported storage nodes carry an `import_state` block (`is_root`, `provider`, `access_mode`, `status`, `synced_at`, `source_id`, `graft_root_id`; `remote_path` omitted) with a legacy `import_metadata` block kept for compatibility. `graft_root_id` is present on **every** node of a grafted tree so a client can tell it is inside a graft, and which folder is the root, without walking parents. `grafted_by` (`{user_id}` — the workspace member who connected the source, never the cloud account) is returned on the graft ROOT only, and **only in workspace context**: it is omitted from share-served responses, where the recipient may be external or anonymous. Change delivery is webhook-driven, with polling as the reconciliation pass behind it: a source whose webhook subscription is live (and does not expire before the next poll) is polled every 6 hours instead of hourly, and one with no live subscription keeps the hourly cadence. An explicitly configured `sync_interval` is always honoured exactly — the 6-hour back-off applies only to sources left on the 1-hour default. The source record carries `provider` and `is_owner` (whether the CALLER owns the bound cloud connection), so a client does not have to join sources against the identity list to gate owner-only controls — source create, pause/resume, disconnect and access-mode changes are identity-owner-only, with no workspace-admin override on create. Sources with >1000 files use async disconnect; when one of those cannot complete its cleanup it normally settles in `disconnect_pending` — not `disconnected` — with the reason in the source's `error_message`, syncing and writing back in neither direction, and accepts the disconnect being sent again; that is the usual outcome rather than a guarantee, so read the source's current `status` instead of assuming it. Import GET endpoints support `format=md`. **Membership governs an import for its whole life, not only at create time.** Sync and write-back re-check, immediately before contacting the provider, that the identity owner still holds a live workspace membership at Member or above and that the workspace and its parent org are still available — so a demotion, a lapsed membership, a removal, or a closed org stops an import instead of leaving it running on a credential nobody is entitled to use any more. When a member is removed from the workspace **or from the org**, their provider identities are revoked and each of their sources flips to `read_only` and `disconnect_pending`: inbound sync and outbound write-back both stop. Content already imported is kept, and remains in the workspace as ordinary storage. Two write-back outcomes worth planning for. **A refusal from the provider's permission system is permanent, not retried** — the job fails with a message naming the remedy (re-share the remote folder to the import agent with write access), which is the failure a read-only grant produces; fix the grant, then `retry`. And **renaming or moving an imported file inside the workspace pushes the content to the new remote path but leaves the object at the old path where it is**, so the user ends up with a duplicate in their own cloud (and the old path can be re-imported on the next sync). Deleting the old object is deliberately not attempted: nothing records a trustworthy pre-rename path, so such a delete could remove a file the user has since put there. ## Common Patterns - List endpoints return arrays in a `response` wrapper with consistent pagination - All profile operations require membership with sufficient permissions - Owner > Admin > Member > Guest permission hierarchy - `"me"` can be used as user_id to reference the authenticated user - Profile path parameters accept either a 19-digit numeric ID or a custom name - Storage operations (workspace and share) follow identical patterns - AI chat endpoints (workspace and share) follow identical patterns - Member management endpoints (org, workspace, share) follow identical patterns - Long-polling supported on activity/poll and upload/details endpoints - Most POST endpoints use `application/x-www-form-urlencoded` bodies; comments use `application/json` ## Additional Resources - Full single-file reference: https://api.fast.io/current/llms/full/ - Agent integration guide: available at `/current/agents/` - MCP Server: `https://mcp.fast.io/mcp` (Streamable HTTP) or `https://mcp.fast.io/sse` (legacy SSE) - MCP Skills: available at `/skill.md` on the MCP server