> Part of the Fastio API Reference. Overview: https://api.fast.io/current/llms/ # Share Management Base URL: `https://api.fast.io/current/` Shares are purpose-built portals for exchanging files with internal teams and external guests. They support branded file preview, download controls, guest access, password protection, expiration, AI-powered features, and real-time collaboration. --- ## Share Types | Type | Direction | Description | Guest Uploads | "Anyone with the link" Access | |------------|---------------|----------------------------------------------------|---------------|-------------------------------| | `send` | Owner -> Guest | Owner distributes files; guests can download only | No | Allowed | | `receive` | Guest -> Owner | Owner collects files; guests can upload only | Yes | Allowed (sign-in to upload unless anonymous uploads are on) | | `exchange` | Bidirectional | Both parties can upload and download | Yes | Allowed (sign-in to upload unless anonymous uploads are on) | **Share type depends on storage mode.** The share type is constrained by the `storage_mode` you create the share with: - **Portal shares (`storage_mode=independent`, the default) are always `send`.** When a share is created in the default portal storage mode, the share type is forced to `send` regardless of the `share_type` you pass — a `receive` or `exchange` value is silently overridden to `send`. A default create (no `storage_mode`, no `share_type`) therefore yields a **Send** share. - **`receive` and `exchange` require `storage_mode=workspace_folder`.** Only workspace-folder-backed shares honor a `receive` or `exchange` `share_type`; portal shares cannot be either type. To create a Receive or Exchange share, create it as a `workspace_folder` share (see Storage Modes below). --- ## Storage Modes (Immutable at Creation) ### Portal (`storage_mode=independent`, default) The share has its own isolated storage. Files added to the portal are independent of any workspace. Changes to workspace files do not affect the portal, and vice versa. **Portal shares are always `send` type** — the share type is forced to `send` at creation and a `receive`/`exchange` request is silently overridden. Use `storage_mode=workspace_folder` to create a Receive or Exchange share. **Features:** Expiration dates, archiving, password protection, custom branding, guest access, inline file preview, download controls, post-download messaging, AI auto-titling. ### Shared Folder (`storage_mode=workspace_folder`) The share is backed by a specific workspace folder. The share displays the live contents of that folder -- files added, updated, or removed in the workspace folder are immediately reflected in the share. No file duplication, so no extra storage cost. **Creation:** Pass `folder_node_id={folder_opaque_id}` to link an existing folder, or `create_folder=true` with `folder_name` to create a new one. **Restrictions:** Expiration dates and archiving are not allowed since the content is live. Each workspace folder can only be shared once. If the backing folder is deleted, the share becomes orphaned (`is_orphaned: true` in details response). Both modes look the same to share recipients -- a branded portal with file preview, download controls, and all share features. ### Response Field: `share_category` API responses include a `share_category` field alongside `storage_mode`. The `storage_mode` field is the input parameter used when creating shares; `share_category` is a response-only field that provides an alternate label. | `storage_mode` | `share_category` | |--------------------|-------------------| | `independent` | `portal` | | `workspace_folder` | `shared_folder` | --- ## Access Options The `access_options` parameter controls who can access the share: | Value | Description | |-------------------------------------------------|---------------------------------------------------------------------| | `'Only members of the Share or Workspace'` | Most restrictive (default). Only explicit members or workspace members. | | `'Members of the Share, Workspace or Org'` | Org members can also access | | `'Anyone with a registered account'` | Any authenticated Fastio user | | `'Anyone with the link'` | Public access. Enables password protection. | **Restrictions:** - Receive and Exchange shares may use `'Anyone with the link'`, but guests must sign in to upload unless `anonymous_uploads_enabled` is true (requires premium plan). - Enabling comments or certain notification settings on a share with `'Anyone with the link'` access automatically upgrades access to `'Anyone with a registered account'` because those features require user identity. - Changing share type to `receive` or `exchange` while access is `'Anyone with the link'` automatically upgrades access to `'Anyone with a registered account'` (unless `anonymous_uploads_enabled` is true). - Changing access away from `'Anyone with the link'` or changing type away from Receive/Exchange automatically disables `anonymous_uploads_enabled`. --- ## Compact Responses (`output=`) Every endpoint that returns one or more share objects (details, list, discovery) accepts an optional `output` query parameter that selects the response shape. 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. | Level | Fields returned on each share (cumulative) | |-------|--------------------------------------------| | `terse` | `id`, `title`, `share_type`, `share_level`, `share_root_id` (the share-root node id as a top-level scalar — member-only; guests do not receive this field), `creator` (scalar user id) | | `standard` | terse + `share_category`, `storage_mode`, `folder_node_id`, full `share_link` object, parent linkage, lifecycle flags (including `locked`, admin-only), `custom_name`, `custom_url`, `description`, `download_security`, `expires`, `created`, `updated`, `guest_chat_enabled`/`anonymous_uploads_enabled` flags, `user_status`, `intelligence` | | `full` | standard + `capabilities`, permission blocks, activity tracking, `comments`, `event_flow`, `filesystem`, `invite`, `member_visibility`, `multiplayer`, `chat`, `search`, `assets`, `access_options`, `display_type`, branding (accent color, logo, background), `password`, `platform`, `notify`, `link_1`/`link_2`/`link_3`, `owner_defined`, `deleted`, `storage`, `suspended`, `parents` | Use `terse` for share pickers, share-link previews, and navigation sidebars — it carries the ID, title, type (Send/Receive/Exchange), access level, the share's root node id as a top-level `share_root_id` scalar (what layouts and transfer pages need to scope navigation), and a `creator` user id so "Shared by {creator}" rows can resolve via the user cache without pulling the full share-link object. Use `standard` for share list views and the summary card on share detail pages — it adds storage mode, folder binding, the full `share_link` object, timestamps, expiration, the caller's membership status, the `locked` lifecycle chip (admin-only), and the `download_security` badge (`high`/`medium`/`off`) that share-list rows render. Use `full` (or omit the parameter) for the share settings screen, branding editors, password configuration, invite link management, and any workflow that reads capability or permission matrices. 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 in `llms.txt` for the full contract. --- ## Create Share ``` POST /current/workspace/{workspace_id}/create/share/ ``` Create a new share in a workspace. **Auth required.** Subject to the organization's and the workspace's sharing policy — both must allow shares, with the org acting as a ceiling. Read `capabilities.can_create_share` on the workspace's details response to know in advance whether the calling user may create one; see *Sharing Policy* in the Workspaces reference. When policy refuses a shared-folder create, the folder is not created either. ### Path Parameters | Parameter | Type | Required | Description | |------------------|--------|----------|---------------------------------------| | `{workspace_id}` | string | Yes | 19-digit numeric workspace profile ID | ### Request Body **Required parameters:** | Parameter | Type | Required | Description | |----------------|--------|----------|-------------------------------------------------------------------| | `intelligence` | string | Yes | Boolean string (`"true"` / `"false"`). Enable Deep Indexing. Setting `"true"` requires both the `content_ai` and `ai_agent` plan features; plans without both will be rejected with `1605 (Invalid Input)`. Portal (`independent`) shares only — workspace folder shares cannot enable Deep Indexing. | **Optional parameters:** | Parameter | Type | Required | Default | Constraints | Description | |----------------------|---------|----------|-----------------|----------------------------------|-----------------------------------------------------------------------------| | `share_type` | string | No | `send` (portal) / `exchange` (workspace folder) | `send`, `receive`, `exchange` | Share direction type. **Portal (`independent`) shares are always `send`** — a `receive`/`exchange` value is silently overridden. `receive`/`exchange` require `storage_mode=workspace_folder`. | | `custom_url` | string | No | `null` | 10-100 chars | Custom URL name for linking to the share. Not auto-generated (the auto-generated URL name is `custom_name`). Send `"null"` to leave unset. Distinct from `title`. | | `access_options` | string | No | `Only members of the Share or Workspace` | See Access Options | Who can access the share | | `invite` | string | No | `owners` | `owners`, `guests` | Who can manage share invitations | | `storage_mode` | string | No | `independent` | `independent`, `workspace_folder`| Storage mode (immutable after creation) | | `folder_node_id` | string | No | - | Valid opaque ID | Workspace folder opaque ID (required for `workspace_folder` if not creating)| | `create_folder` | string | No | - | Boolean string | Create a new backing folder (with `folder_name`) | | `folder_name` | string | No | `Shared Folder` | 1-255 characters | Name for the new backing folder (with `create_folder=true`) | | `title` | string | No | - | 2-80 chars | Share display title | | `description` | string | No | - | 10-500 chars | Share description | | `custom_name` | string | No | Auto-generated | 4-80 chars, URL-friendly | URL-friendly custom name. Auto-generated opaque ID if omitted. | | `password` | string | No | - | 4-128 chars | Password. Only with `'Anyone with the link'` access. | | `expires` | string | No | - | datetime (`YYYY-MM-DD HH:MM:SS`) | Expiration date. Portal mode only, not allowed on shared_folder. | | `notify` | string | No | `never` | `never`, `notify_on_file_received`, `notify_on_file_sent_or_received` | Notification preference | | `comments_enabled` | string | No | - | Boolean string | Enable comments | | `download_security` | string | No | - | `high`, `medium`, `off` | Download security level. `high`: downloads disabled. `medium`: downloads require a short-lived nonce (see storage docs). `off`: downloads unrestricted. | | `guest_chat_enabled` | string | No | - | Boolean string | Enable guest AI chat | | `display_type` | string | No | - | Not blank | Visual display mode (`grid`, `list`) | | `workspace_style` | string | No | - | Not blank | Workspace visual style | | `accent_color` | string | No | - | Valid JSON | JSON color object for accent | | `background_color1` | string | No | - | Valid JSON | JSON color object for primary background | | `background_color2` | string | No | - | Valid JSON | JSON color object for secondary background | | `background_image` | integer | No | - | Numeric, validated range | Background image selection | | `link_1` | string | No | - | Valid JSON | JSON link object (custom link #1) | | `link_2` | string | No | - | Valid JSON | JSON link object (custom link #2) | | `link_3` | string | No | - | Valid JSON | JSON link object (custom link #3) | | `owner_defined` | string | No | - | Valid JSON or `"null"` | Custom owner-defined properties | | `anonymous_uploads_enabled` | string | No | `false` | Boolean string | Enable anonymous file uploads. Receive/Exchange + `'Anyone with the link'` + premium plan only. | ### Request Example ```bash curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/create/share/" \ -H "Authorization: Bearer {jwt_token}" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "share_type=send&intelligence=true&access_options=Anyone+with+the+link&title=Client+Deliverables" ``` ### Response **Status:** `200 OK` ```json { "result": true, "share": { "id": "9876543210987654321", "custom_name": "6lrs43jnglqu2ysgpyovmbgdtxa7c", "storage_mode": "independent" } } ``` For workspace folder shares, the response also includes `folder_node_id`. The create response does not include `share_category` — that response-only label appears on the details/list endpoints. ### Response Fields | Field | Type | Description | |---------------------------|--------|-------------------------------------------------------| | `share.id` | string | 19-digit share profile ID | | `share.custom_name` | string | URL name (custom or auto-generated) | | `share.storage_mode` | string | `independent` or `workspace_folder` | | `share.folder_node_id` | string | Backing folder node ID (workspace_folder only) | ### Error Responses **Reading the error tables:** 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. | HTTP Status | Code | Message | Cause | |-------------|--------------------------|-----------------------------------------------------------------------------|-------------------------------------------------------------| | 406 | `1605 (Invalid Input)`| "An invalid share custom name was supplied." | Custom name fails validation | | 406 | `1605 (Invalid Input)`| "An invalid share expiration date was supplied." | Invalid datetime format | | 406 | `1605 (Invalid Input)`| "Password can only be set for shares with 'Anyone' access option." | Password on non-public share | | 406 | `1605 (Invalid Input)`| "Workspace folder shares cannot have an expiration date..." | Expiration on workspace_folder share | | 406 | `1605 (Invalid Input)`| "Must provide folder_node_id or set create_folder=true..." | Missing folder config for workspace_folder mode | | 412 | `1685 (Feature Limit)` | "The organization has reached its share creation limit..." | Share quota exceeded for billing plan | | 403 | `1700 (Forbidden)` | "Sharing is turned off for this workspace." | Org or workspace sharing policy refuses new shares. `params.reason` = `policy_sharing_disabled`. Distinct from the plan-limit refusal above. | | 406 | `1658 (Not Acceptable)` | "The supplied share custom name is already in use." | Duplicate custom name | | 406 | `1658 (Not Acceptable)` | "This folder has already been shared." | Folder already linked to another share | | 409 | `1660 (Conflict)` | "Unable to process share creation request due to concurrent operation." | Concurrent operation conflict | | 500 | `1663 (Update Failed)` | "There was an internal error processing your create request." | Internal error | --- ## Share Details ``` GET /current/share/{share_id}/details/ ``` Get full share details. **Auth required (optional for public shares).** `{share_id}` accepts a 19-digit numeric ID or a custom name. ### Path Parameters | Parameter | Type | Required | Description | |--------------|--------|----------|--------------------------------------| | `{share_id}` | string | Yes | 19-digit share profile ID or custom name | ### Request Example ```bash curl -X GET "https://api.fast.io/current/share/1234567890123456789/details/" \ -H "Authorization: Bearer {jwt_token}" ``` ### Response **Status:** `200 OK` ```json { "result": true, "share": { "id": "1234567890123456789", "title": "Q4 Financial Reports", "description": "Quarterly financial documents", "share_type": "send", "custom_name": "q4-reports", "storage_mode": "independent", "share_category": "portal", "closed": false, "archived": false, "share_level": "admin", "share_root_id": "2ow3buqu5mdgt4tmjw7ucxcrwhins", "download_security": "off", "activity_tracking": { "enabled": true, "owner_activity": true, "guest_activity": true, "own_activity": true, "all_upload_activity": false }, "comments": { "enabled": true, "owner_comments_visible": true, "guest_comments_visible": true, "personal_replies_visible": true, "owner_replies_visible": true }, "filesystem": { "file_creation": true, "file_modification": "owned", "file_download": "all", "file_view": "all", "folder_creation": true, "folder_modification": "owned" }, "event_flow": { "enabled": true, "can_see_own_events": true, "can_see_owner_events": true, "can_see_guest_events": false }, "multiplayer": { "enabled_for_user": true, "enabled_for_owners": true, "enabled_for_guests": true, "owners_can_see_guests": true, "guests_can_see_owners": false }, "member_visibility": { "user_can_see_members": true, "owners_can_see_members": true, "guests_can_see_members": false }, "invite": { "setting": "owners", "can_invite": true }, "capabilities": { "can_archive": true, "can_set_expiration": true }, "parent_type": "workspace", "parent_workspace": "9876543210987654321", "parent_org": "1122334455667788990", "created": "2024-01-01 10:00:00 UTC", "expires": "2024-12-31 23:59:59 UTC" } } ``` ### Response Fields | Field | Type | Description | |------------------------------------|--------------|--------------------------------------------------------------| | `share.id` | string | Share profile ID | | `share.title` | string\|null | Display title | | `share.description` | string\|null | Share description | | `share.share_type` | string | `send`, `receive`, or `exchange` | | `share.custom_name` | string\|null | URL-friendly custom name | | `share.storage_mode` | string | `independent` or `workspace_folder` | | `share.share_category` | string | `portal` (independent) or `shared_folder` (workspace_folder). Response-only. | | `share.closed` | boolean | Whether the share has been soft-deleted | | `share.archived` | boolean | Whether the share is archived | | `share.share_level` | string | Current user's effective access level: `admin` (share owner/admin, or member-or-above of the parent workspace), `member`, `guest`, `public` (non-member viewing a public share), `excluded` | | `share.share_root_id` | string\|null | Node OpaqueId of the share's root folder (graft point). Top-level scalar form of `share_link.graft_point`. **Member-only.** Returned only when the caller has at least share-member access. | | `share.download_security`| string | Download security level: `high`, `medium`, or `off` | | `share.activity_tracking`| object | Activity visibility settings | | `share.comments` | object | Comment visibility and permission settings | | `share.filesystem` | object | File and folder operation permissions | | `share.event_flow` | object | Real-time event visibility settings | | `share.multiplayer` | object | Real-time collaboration/presence settings. Automatically determined based on share configuration -- not directly configurable via API. | | `share.member_visibility`| object | Who can see other share members | | `share.invite` | object | Invitation policy settings | | `share.capabilities` | object | Operations allowed based on storage mode. Member+ only and full output only. Also carries `can_invite_external` (boolean) — whether the **calling user** may currently bring an outside person onto this share, combining the org's collaboration policy, the caller's role/override, and this share's own `external_invites` flag. Server truth; do not recompute it from `external_invites`. | | `share.external_invites` | string\|null | This share's own external-invite setting: `allowed`, `denied`, or `null` to inherit the org's collaboration policy. Not the effective answer — see `capabilities.can_invite_external` below and *Collaboration Policies* in `llms/orgs.txt`. | | `share.parent_type` | string\|null | `workspace` or `user` | | `share.parent_workspace` | string\|null | Parent workspace ID (workspace-owned shares only) | | `share.parent_org` | string\|null | Parent organization ID (workspace-owned shares only) | | `share.created` | string | Creation timestamp (`YYYY-MM-DD HH:MM:SS UTC`) | | `share.expires` | string\|null | Expiration timestamp, or null | | `share.folder_node_id` | string\|null | Backing folder node ID (workspace_folder only) | | `share.is_orphaned` | boolean | Whether backing folder was deleted (workspace_folder only) | ### Error Responses | HTTP Status | Code | Message | Cause | |-------------|-------------------|-------------------------------------------------|------------------------------------| | 401 | `1650 (Authentication Invalid)`| "Authentication required" | Non-public share without auth | | 401 | 144499 | "You do not have permissions to view this share."| Lacks view permission | --- ## Public Share Details ``` GET /current/share/{share_id}/public/details/ POST /current/share/{share_id}/public/details/ ``` Returns a comprehensive view of a share from a single API call, designed for public share landing pages. Includes share details, owner information, file listing, member list, and comments. **No auth required (optional; enhances permissions if provided).** For password-protected shares, pass the password JWT in the `x-ve-password` header (obtained from the password auth endpoint). ### Request Example ```bash # Public access (no auth) curl -X GET "https://api.fast.io/current/share/1234567890123456789/public/details/" # Password-protected share curl -X GET "https://api.fast.io/current/share/1234567890123456789/public/details/" \ -H "x-ve-password: {password_jwt_token}" # Authenticated user curl -X GET "https://api.fast.io/current/share/1234567890123456789/public/details/" \ -H "Authorization: Bearer {jwt_token}" ``` ### Response **Status:** `200 OK` ```json { "result": true, "share": { "id": "1234567890123456789", "title": "Q4 Financial Reports", "share_type": "send", "storage_mode": "independent", "share_category": "portal" }, "owner": { "id": "9988776655443322110", "account_type": "human", "status": "active", "first_name": "Jane", "last_name": "Smith", "profile_pic": null }, "nodes": [ { "id": "2qk7d-kri4y-yievb-q5hri-eq4io-hij5", "type": "file", "name": "report.pdf", "parent": "root", "size": 1048576 } ], "users": [ { "id": "1122334455667788990", "account_type": "human", "status": "active", "first_name": "John", "last_name": "Doe", "permissions": "guest" } ], "comments": [], "org": { "id": "5566778899001122334", "name": "Acme Corp" } } ``` ### Response Fields | Field | Type | Description | |---------------------|-------------|----------------------------------------------------------------------------| | `share` | object | Full share details (same as details endpoint) | | `owner` | object | Share owner's user resource. `email_address` is included only when the caller is a member of the share; anonymous and non-member visitors receive the owner without it | | `nodes` | array | Top-level files and folders in the share root | | `users` | array | Share members (if user has member visibility permission) | | `comments` | array | Currently always an empty array | | `org` | object\|null| Organization resource (only for workspace-owned shares) | ### Error Responses | HTTP Status | Code | Message | Cause | |-------------|-------------------|-------------------------------------------------|------------------------------------| | 401 | 183836 | "You do not have permissions to view this share."| Lacks public view permission | | 500 | 121168 | "Failed to access share storage" | Internal storage error | --- ## Update Share ``` POST /current/share/{share_id}/update/ ``` Update share settings. **Auth required. Owner/admin only.** Supports partial updates -- only provided fields are modified. Works on archived shares. ### Request Body All fields are optional: | Parameter | Type | Constraints | Description | |------------------------|---------|-----------------------------------|---------------------------------------------------------------------------| | `custom_url` | string | 10-100 chars, `"null"` to clear | Custom URL name for linking to the share | | `title` | string | Not blank, or `"null"` to clear | Display title (2-80 chars) | | `description` | string | `"null"` or `""` to clear | Share description (10-500 chars) | | `custom_name` | string | 4-80 chars, or `"null"` to clear | URL-friendly custom name. Must be unique. | | `share_type` | string | `send`, `receive`, `exchange` | Share direction type. Portal (`independent`) shares stay `send` — a `receive`/`exchange` value is silently overridden. | | `access_options` | string | See Access Options | Who can access the share | | `external_invites` | string | `allowed`, `denied` | This share's own external-invite switch. Ungated, object-admin only — the org-level policy tightening is what needs the Enterprise plan, not this per-object flag. There is currently no way to reset it back to "inherit" once set; see *Collaboration Policies* in `llms/orgs.txt`. | | `invite` | string | `owners`, `guests` | Who can manage invitations | | `password` | string | 4-128 chars, `"null"`/`""` to clear | Password. Only with `'Anyone with the link'` access. | | `expires` | string | datetime, `"null"` to clear | Expiration. Portal mode only. | | `notify` | string | See notification values | Notification preference | | `comments_enabled` | string | Boolean string | Enable/disable comments | | `download_security` | string | `high`, `medium`, `off` | Download security level. `high`: downloads disabled. `medium`: downloads require a short-lived nonce. `off`: downloads unrestricted. | | `display_type` | string | `grid`, `list` | Visual display mode | | `workspace_style` | string | Not blank | Workspace visual style | | `guest_chat_enabled` | string | Boolean string | Enable/disable guest AI chat | | `intelligence` | string | Boolean string | Can be toggled. Setting `"true"` requires both `content_ai` and `ai_agent` plan features. Disabling flushes embeddings; re-enabling re-indexes (costs AI credits). | | `accent_color` | string | JSON or `"null"` | JSON color object for accent | | `background_color1` | string | JSON or `"null"` | JSON color object for primary background | | `background_color2` | string | JSON or `"null"` | JSON color object for secondary background | | `background_image` | integer | Numeric, validated range | Background image selection | | `link_1` | string | JSON or `"null"` | JSON link object (custom link #1) | | `link_2` | string | JSON or `"null"` | JSON link object (custom link #2) | | `link_3` | string | JSON or `"null"` | JSON link object (custom link #3) | | `owner_defined` | string | JSON or `"null"` | Custom owner-defined properties | | `share_link_node_id` | string | `"null"` only | Can only be set to `"null"` to remove workspace share link node | | `anonymous_uploads_enabled` | string | Boolean string | Enable/disable anonymous file uploads. Receive/Exchange + `'Anyone with the link'` + premium plan only. Automatically disabled when access changes from "Anyone" or type changes from Receive/Exchange. | ### Request Example ```bash curl -X POST "https://api.fast.io/current/share/1234567890123456789/update/" \ -H "Authorization: Bearer {jwt_token}" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "title=Updated+Title&description=New+description" ``` ### Response **Status:** `200 OK` ```json { "result": true } ``` ### Important Behaviors - **Password auto-clear**: Changing access away from `'Anyone with the link'` automatically clears the password. - **Access auto-upgrade**: Enabling comments or notification settings that require user identity automatically upgrades access from `'Anyone with the link'` to `'Anyone with a registered account'`. - **Share type auto-upgrade**: Changing to `receive` or `exchange` while access is `'Anyone with the link'` upgrades access to `'Anyone with a registered account'` (unless `anonymous_uploads_enabled` is true). - **Deep Indexing**: Can be enabled and disabled. Enabling requires both the `content_ai` and `ai_agent` plan features — plans that lack either are rejected with `1605 (Invalid Input)`. Disabling destroys indexed embeddings (vector index is flushed). Re-enabling incurs re-indexing costs (AI credits consumed to re-index all files). - **Expiration**: Workspace folder shares cannot have expiration dates. Expiration is validated against the billing plan. - **Share link sync**: A workspace share link node carries the share's effective link name — its title, or its custom URL when it has no title. Updating the title (or the custom URL of a share with no title) renames the link node to match; an update that would leave a linked share with neither is refused. ### Error Responses | HTTP Status | Code | Message | Cause | |-------------|--------------------------|-----------------------------------------------------------------------------|------------------------------------------------| | 406 | `1605 (Invalid Input)`| "The external invite setting must be \"allowed\" or \"denied\"." | Invalid `external_invites` value | | 403 | `153207` | "Widening this share's access is not permitted here by policy." | Widening `access_options` (including `'Anyone with a registered account'` → `'Anyone with the link'`) while the org's collaboration policy denies the acting user. `params.reason` = `external_invites_denied` or `external_invites_object_denied` — see *Collaboration Policies* in `llms/orgs.txt`. | | 500 | `191268` | "The policy that governs sharing this share could not be determined. Please try again." | The policy could not be evaluated (transient — retry). | | 406 | `1605 (Invalid Input)`| "An invalid share custom name was supplied." | Invalid or duplicate custom name | | 406 | `1605 (Invalid Input)`| "An invalid share expiration date was supplied." | Invalid datetime format | | 406 | `1605 (Invalid Input)`| "A share linked into workspace storage must keep a title or custom URL; set one before clearing the other." | The update would leave a linked share with no name for its link node | | 406 | `1605 (Invalid Input)`| "Password can only be set for shares with 'Anyone' access option." | Password on non-public share | | 406 | `1605 (Invalid Input)`| "Deep Indexing cannot be enabled on workspace folder shares. Only portal shares with independent storage support Deep Indexing." | Deep Indexing enable on a `workspace_folder` share | | 406 | `1605 (Invalid Input)`| "Deep Indexing requires a plan upgrade. This feature is not available on your current subscription plan." | Plan missing `content_ai` or `ai_agent` (cannot set `intelligence=true`) | | 403 | *(generated)* | "Your organization's AI policy does not allow enabling this here." | Turning `intelligence=true` **ON** while the org's AI policy denies the caller Deep Indexing for this share's parent workspace. `params.reason` = `ai_policy_denied` (`params.feature:"intelligence"`) or `ai_policy_workspace_not_allowed`. Turning it **off** is never refused by this policy. See *AI, Deep Indexing & MCP Access Policy* in `llms/orgs.txt`. | | 412 | `1685 (Feature Limit)` | "The Deep Indexing setting can only be changed twice per minute and five times per hour. Please wait and try again." | Deep Indexing toggle throttled (per-share limit) | | 406 | `1605 (Invalid Input)`| "Workspace folder shares cannot have an expiration date..." | Expiration on workspace_folder share | | 401 | 144499 | "You do not have permissions to access this share." | User lacks admin permission | | 406 | `1658 (Not Acceptable)` | "The supplied share custom name is already in use." | Duplicate custom name | | 503 | `174399` | "The plan that governs this share could not be determined. Please try again." | Setting `expires` requires reading the plan that owns this share, and that read failed. **Transient — retry.** It says nothing about the date you sent, and nothing about your plan | | 503 | `140240` | "The plan that governs this share could not be determined. Please try again." | The same guard on the `anonymous_uploads_enabled` path. **Transient — retry.** Do not read it as "your plan is too low" — that refusal is the separate feature-limit error | | 500 | `1663 (Update Failed)` | "There was an internal error processing your update request." | Internal error | --- ## Delete Share ``` DELETE /current/share/{share_id}/delete/ ``` Soft-delete (close) a share. **Auth required. Owner/admin only.** Requires confirmation. ### Query Parameters | Parameter | Type | Required | Description | |-----------|--------|----------|----------------------------------------------------------------------------------| | `confirm` | string | Yes | Must match either the share's `custom_name` (case-insensitive) or its numeric `id`. Send as a query-string parameter (`?confirm=...`) — a request body is not read on this `DELETE`. Safety check. | ### Request Example ```bash curl -X DELETE "https://api.fast.io/current/share/1234567890123456789/delete/?confirm=my-share-name" \ -H "Authorization: Bearer {jwt_token}" ``` ### Response **Status:** `202 Accepted` ```json { "result": true } ``` ### Error Responses | HTTP Status | Code | Message | Cause | |-------------|--------------------------|----------------------------------------------------------|------------------------------------| | 406 | 130987 | "The `confirm` field is required..." | `confirm` not supplied as a query parameter | | 406 | 10571 | "The `confirm` field provided does not match the share's `custom_name` or `id`." | Confirmation mismatch | | 401 | 144499 | "You do not have permissions to access this share." | Lacks admin permission | | 500 | `1663 (Update Failed)` | "There was an internal error processing your request." | Internal error | ### Notes - Soft delete with retention period before permanent removal. - For workspace folder shares, the share reference in the backing folder is cleaned up. - Share link nodes in the parent workspace are also removed. --- ## Archive / Unarchive ``` POST /current/share/{share_id}/archive/ POST /current/share/{share_id}/unarchive/ ``` Archive or unarchive a share. **Auth required. Owner/admin only.** Portals only -- workspace folder shares cannot be archived. No request body required. ### Request Example ```bash curl -X POST "https://api.fast.io/current/share/1234567890123456789/archive/" \ -H "Authorization: Bearer {jwt_token}" ``` ### Response **Status:** `202 Accepted` ```json { "result": true } ``` ### Error Responses | HTTP Status | Code | Message | Cause | |-------------|--------------------------|---------------------------------------------------------------|------------------------------------------| | 406 | `1605 (Invalid Input)`| "Workspace folder shares cannot be archived..." | workspace_folder share | | 500 | `1663 (Update Failed)` | "The share is already archived." / "The share is not archived."| Wrong current state | | 401 | 144499 | "You do not have permissions to access this share." | Lacks admin permission | ### Notes - Archived shares block guest access. - Unarchiving requires the share unarchive feature to be enabled on the billing plan. --- ## Password Protection Available only on shares with `'Anyone with the link'` access. ### Set/Update Password Set via `password` parameter on create or update (4-128 chars). Send `"null"` or `""` to clear. ### Authenticate with Password ``` POST /current/share/{share_id}/auth/password/ ``` Authenticate with a share password to get a scoped JWT token. **No user auth required.** #### Request Body | Parameter | Type | Required | Description | |------------|--------|----------|-------------------| | `password` | string | Yes | Share password | #### Request Example ```bash curl -X POST "https://api.fast.io/current/share/1234567890123456789/auth/password/" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "password=mysecretpassword" ``` #### Response **Status:** `200 OK` ```json { "result": true, "expires_in": 86400, "auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` #### Response Fields | Field | Type | Description | |------------------------|---------|-------------------------------------------------| | `expires_in` | integer | Token lifetime in seconds (86400 = 24 hours) | | `auth_token` | string | JWT token to use in `x-ve-password` header | #### Error Responses | HTTP Status | Code | Message | Cause | |-------------|---------------------|------------------------------------------------------------|----------------------------------------| | 406 | `1605 (Invalid Input)` | "Password is required for authentication." | Missing password field | | 401 | `1650 (Authentication Invalid)` | "This share does not require password authentication." | Not password-protected or not public | | 406 | `1658 (Not Acceptable)`| "Invalid password provided for this share." | Wrong password | | 429 | `10776` | "Too many failed password attempts for this share. Try again in N minutes." | Too many recent wrong passwords for this share — a temporary, self-clearing pause | | 500 | `1654 (Internal Error)`| "Failed to create authentication token." | JWT creation failure | #### Notes - Token expires after 24 hours. - If the share's password is changed, all previously issued tokens become invalid. - Use the returned token in the `x-ve-password` header for subsequent share requests. - **Repeated wrong passwords temporarily block password entry for the share.** While the block lasts, every attempt on that share is refused — even with the correct password — with `429`, `error.code` `10776`, and `error.params.retry_after_seconds`, the number of seconds until attempts are accepted again. Wait that long before retrying; the block clears by itself. A correct password clears the failure count. --- ## Anonymous File Drop (Guest Auth) Enables anonymous file uploads on public Receive and Exchange shares without requiring account registration. Visitors obtain a scoped JWT via the guest auth endpoint, then use it as a Bearer token for subsequent upload requests. **Requirements:** - Share must have `'Anyone with the link'` access - Share must be Receive or Exchange type - `anonymous_uploads_enabled` must be true on the share - Premium plan required ### Authenticate as Guest ``` POST /current/share/{share_id}/auth/guest/ ``` Obtain a scoped JWT for anonymous file uploads. Creates an ephemeral user account. **No user auth required.** #### Request No body required. POST request only. #### Request Example ```bash curl -X POST "https://api.fast.io/current/share/1234567890123456789/auth/guest/" ``` #### Response **Status:** `200 OK` ```json { "result": true, "auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 86400 } ``` #### Response Fields | Field | Type | Description | |------------------------|---------|------------------------------------------------- | | `auth_token` | string | Scoped JWT for anonymous uploads (Bearer token) | | `expires_in` | integer | Seconds until `auth_token` expires. A newly issued token reports 86400 (24 hours); when the request's existing guest token for this share is returned instead, this is that token's remaining lifetime | #### Error Responses | HTTP Status | Code | Message | Cause | |-------------|--------------------------|-------------------------------------------------------------------|---------------------------------------------------| | 401 | `1650 (Authentication Invalid)` | "Anonymous uploads are not enabled for this share." | Feature not enabled or share not eligible | | 412 | `1685 (Feature Limit)` | "Anonymous uploads require a premium plan." | Org not on premium plan | | 429 | `1671 (Rate Limited)` | Rate limit exceeded | Too many requests from this IP | | 503 | `199934` | "The plan that governs this share could not be determined. Please try again." | The plan behind the share could not be read. **Transient — retry.** It is deliberately not the "requires a premium plan" refusal above: that one is a settled answer, this one means we do not yet know | | 500 | `1654 (Internal Error)` | "Failed to create authentication token." | JWT creation failure | #### Notes - The returned `auth_token` should be used as a Bearer token in the `Authorization` header for subsequent upload requests. - Each call creates a new ephemeral user account scoped to the share, unless the request carries (as its `Authorization: Bearer` header) a guest token already issued for this share with more than an hour left — that same token is returned instead. - Tokens are short-lived; obtain a new token if the previous one expires. ### Anonymous Upload Workflow 1. **Check share eligibility:** `GET /current/share/{share_id}/public/details/` — verify `anonymous_uploads_enabled: true` in the response. 2. **Obtain guest token:** `POST /current/share/{share_id}/auth/guest/` — returns a scoped `auth_token`. 3. **Upload files:** Use the standard upload flow (`POST /current/upload/` to create a session, `POST /current/upload/{upload_id}/chunk/` per chunk, `POST /current/upload/{upload_id}/complete/`) with `Authorization: Bearer {auth_token}`. Create the session with `action=create`, `instance_id={share_id}` and `folder_id` to place the file in the share directly. 4. **Add to share (only if the session had no target):** `POST /current/share/{share_id}/storage/{folder_id}/addfile/` with `name` and `from={"type":"upload","upload":{"id":"{upload_id}"}}`, using the same Bearer token. --- ## Expiration Available only on **portal** shares (not workspace folder). Set the `expires` parameter (datetime format: `YYYY-MM-DD HH:MM:SS`) on create or update. Send `"null"` to clear. When the share expires, access is revoked. Expiration is validated against the billing plan. --- ## Branding & Styling Shares support custom branding through the update endpoint: | Parameter | Type | Description | |---------------------|---------|--------------------------------------| | `accent_color` | string | JSON color object for accent | | `background_color1` | string | JSON color object for primary bg | | `background_color2` | string | JSON color object for secondary bg | | `background_image` | integer | Background image selection (numeric) | | `link_1` | string | JSON link object (custom link #1) | | `link_2` | string | JSON link object (custom link #2) | | `link_3` | string | JSON link object (custom link #3) | Send `"null"` for any JSON field to clear it. --- ## Share Assets ### List Available Asset Types ``` GET /current/share/assets/ ``` Returns the schema of available asset types (e.g., logo, background). **No auth required.** ### List Share Assets ``` GET /current/share/{share_id}/assets/ ``` List assets set on a share. **Auth required. Share members (member role and above) only.** ### Upload/Set Asset ``` POST /current/share/{share_id}/assets/{asset_id}/ ``` Upload an asset (multipart/form-data). **Auth required. Share members (member role and above) only.** | Field | Type | Required | Description | |------------|------|----------|----------------------------------| | `file` | file | Yes | The asset file to upload | | `metadata` | string (JSON object) | No | Optional metadata for the asset, sent as a JSON object string (e.g. `{}`). Invalid JSON or a non-object value is refused with 406. | #### Request Example ```bash curl -X POST "https://api.fast.io/current/share/1234567890123456789/assets/logo/" \ -H "Authorization: Bearer {jwt_token}" \ -F "file=@logo.png" \ -F 'metadata={"crop_x": 0, "crop_y": 0}' ``` ### Delete Asset ``` DELETE /current/share/{share_id}/assets/{asset_id}/ ``` Remove an asset. **Auth required. Share members (member role and above) only.** ### Read Asset Binary ``` GET /current/share/{share_id}/assets/{asset_id}/read/ HEAD /current/share/{share_id}/assets/{asset_id}/read/ ``` Returns raw binary data of a share asset. **Auth optional for public shares.** Works on archived shares. | Query Parameter | Type | Required | Description | |-----------------|---------|----------|-------------------------------------------------------| | `width` | integer | No | Requested width (max 4096). Background images only. | | `height` | integer | No | Requested height (max 4096). Background images only. | --- ## Share Members ### Permission Levels | Level | Value | Description | |--------|-------|---------------------------------------------| | Owner | 1000 | Full control and ownership | | Admin | 500 | Administrative access, manage members | | Member | 100 | Standard member access (default for new) | | Guest | 50 | Limited guest access | | View | 20 | Read-only access | ### Notification Options | Value | Description | |------------------------------------------------|---------------------------------------------| | `"Do not notify me"` | No notifications | | `"Notify me in app"` | In-app only (default) | | `"Notify me in app and via email"` | In-app and email | | `"Notify me via app, email and text message"` | All notification channels | ### Add Member or Send Invitation ``` POST /current/share/{share_id}/members/{email_or_user_id}/ ``` Use a 19-digit user ID to add an existing user directly, or an email address to send an invitation. An existing user who is newly added is sent a notification email with a direct link to the share. **Auth required.** Share members (member role and above) may manage members; guests may too when the share's `invite` setting is `guests`. You cannot add or invite anyone at a role above your own. The parameters are **form fields** (`application/x-www-form-urlencoded` or `multipart/form-data`); a JSON request body is refused with a 406 input error. An omitted `permissions` means `member` for a new membership (re-adding a current (unexpired) member without it keeps their role; an expired membership is restored as `member`), so a caller below member (a guest) must send `permissions=guest` (or `view`); when it is omitted for someone already a member, your role must be at least their current role. If you pass an email address that belongs to an account which has not verified that address, an invitation is sent to it instead of adding the account directly. #### Request Body (Adding Existing User) | Field | Type | Required | Default | Description | |---------------------|--------|----------|----------------------|--------------------------------------------------------------------------------| | `permissions` | string | No | `member` | `admin`, `member`, `guest`, `view`. Cannot be `owner` (refused with a 406; use Transfer Ownership). Omit for the `member` default on a new member (omitting it for a current (unexpired) member keeps their role; an expired membership is restored as `member`); any other value (including `any`) is rejected with a 406 input error. | | `notify_options` | string | No | `"Notify me in app"` | Notification preference | | `expires` | string | No | - | Membership expiration (`YYYY-MM-DD HH:MM:SS UTC`). `null` or `""` to clear. | | `notification` | string | No | - | Send `force` to resend the notification email to an existing member (skipped within 60 seconds of the initial add). | #### Request Body (Inviting by Email) | Field | Type | Required | Default | Description | |----------------------|--------|----------|----------|--------------------------------------------| | `permissions` | string | No | `member` | Permission level for the invitation (same values as above; an invalid value is rejected with a 406 input error) | | `message` | string | No | - | Custom message for the invitation email | | `invitation_expires` | string | No | - | Invitation expiration datetime | #### Request Example ```bash # Add existing user by ID curl -X POST "https://api.fast.io/current/share/1234567890123456789/members/9876543210987654321/" \ -H "Authorization: Bearer {jwt_token}" \ -d "permissions=member" # Invite by email curl -X POST "https://api.fast.io/current/share/1234567890123456789/members/newuser@example.com/" \ -H "Authorization: Bearer {jwt_token}" \ -d "permissions=guest" \ --data-urlencode 'message=Join our shared files!' ``` #### Response (Invitation Created) ```json { "result": true, "invitation": { "id": "aea3w-cuan6-edcu5-vkaex-g52gm-dacr", "inviter": "Jane Smith", "inviter_actor": { "user_id": "9876543210987654321", "kind": "human", "agent_name": null, "name_source": null, "credential_type": "session", "verified": false }, "invitee_email": "newuser@example.com", "invitee_uid": null, "accepted_uid": null, "entity_type": "share", "share": { "id": "1234567890123456789", "name": "Project Files" }, "state": "pending", "consumed": false, "created": "2025-01-15 10:30:00 UTC", "updated": "2025-01-15 10:30:00 UTC", "expires": "2025-02-15 10:30:00 UTC" } } ``` The `share` object is abbreviated here; it is the full share resource. `inviter_actor` says who sent the invitation and whether an agent acted for them (`user_id`, `kind`, `agent_name`, `name_source`, `credential_type`, `verified`); the invitation email names the agent the same way (e.g. "Dobby (via API key) for Jane"). Any `agent_name` other than Fastio's own verified agent is self-declared. Full reference: *Actor Attribution* in the Storage reference. #### Error Responses | HTTP Status | Code | Message | Cause | |-------------|--------------------------|--------------------------------------------------------------------------------|--------------------------------| | 406 | `1692 (Cannot Add As Owner)`| "Adding a member as an owner is not allowed" | Tried to add as owner | | 406 | `1605 (Invalid Input)`| "Invalid permissions value. Valid values are: admin, member, guest, view." | `permissions` is not one of those role names | | 406 | `1605 (Invalid Input)`| "This endpoint does not accept a JSON request body..." | JSON request body sent | | 406 | `1605 (Invalid Input)`| "You cannot create a membership for the share owner." | Target is share owner | | 406 | `1605 (Invalid Input)`| "You cannot create a membership for yourself." | Target is the caller | | 406 | `1605 (Invalid Input)`| "You cannot add, update, or delete a membership with a higher permission..." | Permission exceeds caller | | 401 | `1680 (Access Denied)` | "You do not have permission to manage the members of this Share." | Lacks member edit permission | | 412 | `1685 (Feature Limit)` | "Share invitation limit reached: X of Y invitations used" | Invitation quota exceeded | | 403 | — | Reason-carrying refusal (`params.reason`) | The invitee is external and the org's collaboration policy (or this share's own `external_invites` flag) denies the inviter. `reason` = `external_invites_denied` or `external_invites_object_denied`. Applies to a fresh invite, a resend, a direct add of an existing account, and a permission change or expiry extension that broadens an existing membership; a pure permission reduction is never gated. See *Collaboration Policies* in `llms/orgs.txt`. | ### Remove Member ``` DELETE /current/share/{share_id}/members/{user_id}/ ``` Remove a member. **Auth required.** Same caller rule as Add Member; you cannot remove someone whose role is above your own, and the owner cannot be removed (use Transfer Ownership). ### Leave Share (Self-Removal) ``` DELETE /current/share/{share_id}/member/ ``` Leave a share. **Auth required.** Owners cannot leave; must transfer ownership first. Works on archived/expired shares. ### Member Details ``` GET /current/share/{share_id}/member/{user_id}/details/ ``` Get detailed membership info for a specific user. **Auth required.** #### Response ```json { "result": true, "user": { "id": "9876543210987654321", "account_type": "human", "email_address": "member@example.com", "first_name": "John", "last_name": "Doe", "permissions": "admin", "status": "active", "member_added_at": "2026-08-29 15:48:29 UTC" } } ``` `member_added_at` is when this membership was created (`YYYY-MM-DD HH:MM:SS UTC`). 🔴 **The key is absent, not null, when you are not allowed to see it** — it is emitted only to the member themselves or to an admin-or-above of this share. A peer member gets no `member_added_at` key at all, so read it with a presence check on the key rather than a null check. The same applies here and in List Members to `notify` (present only on your own membership), `expires` and `invite` (omitted when unset). ### Update Member ``` POST /current/share/{share_id}/member/{user_id}/update/ ``` Update permissions, notification settings, or expiration. **Auth required.** Same caller rule as Add Member; you cannot set a role above your own. Form fields only — a JSON request body is refused with a 406 input error. | Field | Type | Required | Description | |-----------------|--------|----------|---------------------------------------------------------------| | `permissions` | string | No | `admin`, `member`, `guest`, `view`. An unrecognised value is rejected with a 406 input error. `owner` is ignored (use Transfer Ownership). | | `notify_options`| string | No | Notification preference; omitted leaves it unchanged | | `expires` | string | No | Membership expiration datetime. `null`/`""` to clear. | ### Transfer Ownership ``` POST /current/share/{share_id}/member/{user_id}/transfer_ownership/ ``` **POST only** — `GET`, `HEAD`, and every other method return 405. Transfer share ownership to another member, who must be an enabled member of the share. **Auth required. Owner only.** Current owner is demoted to admin. The share's parent workspace is never changed. **Fixed behaviour:** the successor is promoted before the current owner is demoted, under a per-share lock. **Retry completes a partial transfer** — if a previous call stopped midway (both users end up owners), calling again with the **same** target finishes it and returns 200. A concurrent call on the same share returns 409 `transfer_in_progress`. No email is sent — this is recorded as an event only. **Response (200 OK):** ```json { "result": true, "ownership": { "profile_id": "5123456789012345678", "profile_type": "share", "previous_owner": "1111111111111111111", "new_owner": "9876543210987654321", "transferred_at": "2026-09-23 16:37:29 UTC" } } ``` **Errors (`error.params.reason`):** 406 `successor_is_self`; 406 `successor_not_member` (not a member, or their membership was removed or has expired); 406 `successor_unavailable` (closed, suspended, or locked — an invited member who has not yet claimed an account is **not** refused here); 409 `transfer_in_progress`; 401 "You must be the owner to transfer ownership of this share." (not the owner) or "You are no longer the owner of this share." (ownership changed while the call was waiting); 403 `scope_admin_required` (the credential is not admin-capable on this share); 500 the transfer did not finish — repeat with the same target; 503 retry. **Event:** `ownership_transferred` (audit log; `profile_type`, `from_user`, `to_user`). The two existing `membership_updated` events (promotion and demotion) still fire. ### List Members ``` GET /current/share/{share_id}/members/list/ ``` List all members. **Auth required.** #### Response ```json { "result": true, "users": [ { "id": "1111111111111111111", "account_type": "human", "email_address": "owner@example.com", "first_name": "Jane", "last_name": "Smith", "permissions": "owner", "status": "active" }, { "id": "5566778899001122334", "account_type": "human", "email_address": "invited@example.com", "first_name": "invited@example.com", "last_name": "", "permissions": "guest", "status": "pending", "invite": { "id": "aea3wcuan6edcu5vkaexg52gmdacr", "created": "2025-01-15 10:30:00", "expires": "2025-01-18 10:30:00" } } ] } ``` ### Pending Members When a user is invited to a share by email but does not yet have a Fastio account, they appear as a **pending member** in the member list. Pending members are placeholders that reserve a seat for the invitee before they sign up. **How pending members appear in responses:** - The `status` field is `"pending"` (vs `"active"` for registered users). - An `invite` object is included with basic invitation details: `id` (the invitation ID, unhyphenated), `created`, and `expires` (the acceptance deadline, or null). It is a snapshot taken when the invitation was sent, with timestamps as `YYYY-MM-DD HH:MM:SS` (UTC, no suffix). - `email_address` shows the invited email address. - `first_name` is set to the invited email address; `last_name` is empty. **Account claim:** When the invited user signs up or accepts the invitation with an existing account, their status transitions from `"pending"` to `"active"`. Their share access is preserved across the transition. **Removal:** To remove a pending member, delete their invitation using the invitation endpoints (see List Invitations, Delete Invitation below). Deleting the invitation removes the pending member. **Notifications:** Pending members do not receive in-app or email notifications until they claim their account. --- ### Join Share ``` POST /current/share/{share_id}/members/join/ POST /current/share/{share_id}/members/join/{invitation_key}/ POST /current/share/{share_id}/members/join/{invitation_key}/accept/ POST /current/share/{share_id}/members/join/{invitation_key}/decline/ ``` Join a share via self-join or invitation. **Auth required.** **Self-join rules** (without invitation key): | Access Option | Who Can Self-Join | |-------------------------------------------------|---------------------------------------------------------------| | `'Only members of the Share or Workspace'` | Members of the share or its parent workspace | | `'Members of the Share, Workspace or Org'` | Members of the share, parent workspace, or parent org | | Other values | Self-join blocked; invitation required | A new self-joined member receives `member` permission with no expiration; a current (unexpired) member who self-joins keeps their role; an expired membership is restored as `member`. Only `notify_options` is read from input; `permissions` is ignored. **Accepting an invitation is re-checked against the collaboration policy at redemption**, not only at issuance — the org policy or the inviter's own standing can have changed since the invite went out. A refusal is HTTP 403 with `params.reason` = `external_invites_denied` or `external_invites_object_denied` and leaves the invitation **pending** (never failed) so the invitee can retry later; **decline is never gated**. See *Collaboration Policies* in `llms/orgs.txt`. ### List Invitations ``` GET /current/share/{share_id}/members/invitations/list/ GET /current/share/{share_id}/members/invitations/list/{state}/ ``` List invitations, optionally filtered by state (`pending`, `accepted`, `declined`). **Auth required. Share admin or above.** Returns `{"result": true, "invitations": [...]}`; each entry has the fields of the invitation object above (`id`, `inviter`, `inviter_actor`, `invitee_email`, `invitee_uid`, `accepted_uid`, `entity_type`, `state`, `consumed`, `created`, `updated`, `expires`) without the `share` object. `invitee_uid` and `accepted_uid` are 19-digit user IDs as strings, or null. ### Update Invitation ``` POST /current/share/{share_id}/members/invitation/{invitation_id}/ ``` Update an invitation. `{invitation_id}` can be an invitation ID or an email address; the invitation must belong to this share, otherwise the request fails with 406 exactly as an unknown invitation does ("Invalid invitation id or Invitation not found." for an ID). **Auth required. Share admin or above.** Form fields only — a JSON request body is refused with a 406 input error. | Field | Type | Required | Description | |-----------------|--------|----------|------------------------------------------------------------------| | `state` | string | No | `pending`, `accepted`, `declined` | | `permissions` | string | No | The role granted when the invitation is accepted: `admin`, `member`, `guest`, `view`. A new value cannot be above your own role and cannot be `owner`; such a request returns an error (406 `127022` / 406 `1692`) and leaves the invitation unchanged | | `notify_options`| string | No | Notification preference applied on acceptance | | `expires` | string | No | New deadline for accepting the invitation (must be in the future). It does not set an expiry on the membership the invitation grants; an empty value or `null` leaves the deadline unchanged | ### Delete Invitation ``` DELETE /current/share/{share_id}/members/invitation/{invitation_id}/ ``` Revoke an invitation. `{invitation_id}` can be an invitation ID or an email address; the invitation must belong to this share, otherwise the request fails with 406 exactly as an unknown invitation does ("Invalid invitation id or Invitation not found." for an ID). **Auth required. Share admin or above.** --- ## Share Discovery ### List All Shares ``` GET /current/shares/all/ ``` List all accessible shares (joined, invited, owned). **Auth required.** Orphaned workspace folder shares are automatically filtered out. Returns full permission and settings objects for each share. Paginated with `limit` (default 100, max 500) and `offset`; the response carries a `pagination` object (`total`, `limit`, `offset`, `has_more`). #### Response Fields Each share in the `shares` array includes the same fields as the details endpoint, plus: | Field | Type | Description | |-----------------------------------|-------------|------------------------------------------------------| | `shares[].user_status` | string | `joined` (you hold a direct membership on the share) or `available` (you can reach it without one, e.g. through its workspace) | | `shares[].folder_node_id` | string\|null| Backing folder node ID (workspace_folder only) | | `shares[].is_orphaned` | boolean | Whether backing folder was deleted | ### List Available Shares ``` GET /current/shares/available/ ``` List shares the user has joined or owns (excludes pending invitations). **Auth required.** Does not include parent workspace/org information. ### Check Share Name Availability ``` GET /current/shares/check/name/{name}/ ``` Check if a share custom name is available. **Auth required.** **Status `202 Accepted`** = name available. | HTTP Status | Code | Message | Cause | |-------------|--------------------------|--------------------------------------------------|--------------------| | 406 | `1605 (Invalid Input)`| "An invalid share name was supplied." | Fails validation | | 406 | `1658 (Not Acceptable)` | "The supplied share name is already in use." | Name already taken | ### List Shares in Workspace ``` GET /current/workspace/{workspace_id}/list/shares/ ``` List shares belonging to a workspace. **Auth required.** Paginated with offset-based pagination (`limit`/`offset`). | Query Parameter | Type | Required | Description | |-----------------|--------|----------|------------------------------------------| | `limit` | integer| No | Number of results to return | | `offset` | integer| No | Number of results to skip | | `archived` | string | No | Boolean string. Filter by archived state.| ### List User's Shares ``` GET /current/user/me/list/shares/ ``` List all shares accessible to the current user (owned, joined, invited). **Auth required.** Paginated (`limit`/`offset`). Includes parent workspace and org information. | Query Parameter | Type | Required | Description | |-----------------|--------|----------|------------------------------------------| | `limit` | integer| No | Number of results to return | | `offset` | integer| No | Number of results to skip | | `archived` | string | No | Boolean string. Filter by archived state.| --- ## Import Share into Workspace ``` POST /current/workspace/{workspace_id}/import/share/{share_id}/ ``` Import a user-owned share into a workspace. **Auth required.** Caller must be the share owner with no other co-owners. The share must currently be user-owned (not already in a workspace). Archived shares are automatically unarchived during import. ### Error Responses | HTTP Status | Code | Message | Cause | |-------------|--------------------------|---------------------------------------------------------------------|----------------------------------| | 406 | `1605 (Invalid Input)`| "This share is not owned by you and cannot be imported." | Share not user-owned | | 406 | `1605 (Invalid Input)`| "The share has multiple owners..." | Multiple co-owners exist | | 412 | `1685 (Feature Limit)` | "The workspace has reached its share limit." | Share quota exceeded | | 500 | `1654 (Internal Error)` | "Failed to import the share to the workspace." | Internal error | --- ## Share AI Shares support AI features when `intelligence` is enabled. Enabling `intelligence` and creating or sending chat messages require plan features (`content_ai` plus `ai_agent` for write/agentic flows). See the [AI reference](https://api.fast.io/current/llms/ai/#plan-requirements) for the full plan matrix. ### Auto-Generate OG Image ``` GET /current/share/{share_id}/ai/autoog/ ``` Returns the share's Open Graph image (binary image response, not JSON). **No auth needed** for shares with `'Anyone with the link'` or `'Anyone with a registered account'` access; every other share returns a generic placeholder image. ### Auto-Generate Title & Description ``` POST /current/share/{share_id}/ai/autotitle/ ``` Generate a title and description for the share based on its contents. **Auth required.** ### AI Agent (Ripley) Share AI follows the same agent thread/turn model as workspace AI. Replace `/workspace/{id}` with `/share/{id}` in all agent endpoints; the request and response shapes are identical, with one exception: message details (`.../message/{message_id}/details/`) returns the turn under a `turn` key on shares, where the workspace endpoint uses `message`. For the full request/response contract (thread objects, turn objects, streaming events, plan requirements), see the [AI reference](https://api.fast.io/current/llms/ai/) — the shapes are defined there and are not duplicated here. **Endpoints:** ``` POST /current/share/{share_id}/ai/agent/ -- create thread GET /current/share/{share_id}/ai/agent/list/ -- list threads GET /current/share/{share_id}/ai/agent/{thread_id}/details/ -- thread details POST /current/share/{share_id}/ai/agent/{thread_id}/update/ -- update thread DELETE /current/share/{share_id}/ai/agent/{thread_id}/ -- delete thread POST /current/share/{share_id}/ai/agent/{thread_id}/message/ -- send message (start a turn) POST /current/share/{share_id}/ai/agent/{thread_id}/cancel/ -- cancel the in-flight turn GET /current/share/{share_id}/ai/agent/{thread_id}/messages/list/ -- list messages GET /current/share/{share_id}/ai/agent/{thread_id}/message/{msg_id}/details/ -- message details GET /current/share/{share_id}/ai/agent/{thread_id}/message/{msg_id}/read/ -- stream response (SSE) POST /current/share/{share_id}/ai/agent/{thread_id}/publish/ -- publish thread (currently disabled platform-wide -- returns 403) POST /current/share/{share_id}/ai/share/ -- AI share markdown ``` --- ## QuickShare (Workspace Feature) > **Deprecated — use File Share.** Creating or extending a QuickShare now returns **403** with a directed message pointing to `POST /current/workspace/{workspace_id}/create/fileshare/`. The durable **File Share** (see the next section) replaces it. Existing QuickShare links can still be viewed, downloaded, and revoked during the drain; the public read endpoints below remain live until the existing population reaches its natural expiry. QuickShare creates a temporary public link for a single file. This is a **workspace** storage feature, not a share management feature, but is related to sharing. ### Create QuickShare (deprecated → 403) ``` POST /current/workspace/{workspace_id}/storage/{node_id}/quickshare/ ``` **Deprecated.** This endpoint returns **403** (`10756 (Quickshare Deprecated)`) for the create and extend-expiry paths. Use `POST /current/workspace/{workspace_id}/create/fileshare/` instead. The historical constraints (single file only, max 1 GB, default expiration 3 hours, maximum 7 days, `expires` / `expires_at` parameters) no longer apply because creation is closed. ### Public Access Endpoints (No Auth Required) #### QuickShare Details ``` GET /current/quickshare/{opaque_id}/details/ ``` Returns metadata including file information, creator, expiration, and download limit status. **Response:** ```json { "result": true, "quickshare": { "id": "a2qtn-6pa4l-5b4vn-gwruj-ibehh-xq4y", "node": { "id": "2qk7d-kri4y-yievb-q5hri-eq4io-hij5", "type": "file", "name": "presentation.pdf", "size": 5242880, "mimetype": "application/pdf", "mimecategory": "document", "summary": {"title": "Quarterly Presentation", "short": "Q4 slides overview"}, "metadata": {"title": "Custom Title", "short": "Custom description"} }, "creator_uid": { "id": "9876543210987654321", "account_type": "human", "first_name": "Jane", "last_name": "Doe" }, "limit_exceeded": false, "expires": "2024-01-15 13:30:00 UTC", "created": "2024-01-15 10:30:00 UTC" } } ``` #### Download File ``` GET /current/quickshare/{opaque_id}/storage/read/ ``` Returns raw binary file content with `Content-Type` and `Content-Disposition` headers. **Transfer Limits:** - Max total bytes: 10 GB - Max multiplier: 100x file size - Once either limit is reached, `limit_exceeded` is set permanently. #### Read Note Content ``` GET /current/quickshare/{opaque_id}/storage/readnote/ ``` Returns the content of a note/markdown file as JSON. Unlike the binary `/read/` endpoint, this returns the sanitized markdown content as a string within the JSON response along with the note resource. **Response:** ```json { "result": true, "content": "# Note content here\n\nMarkdown text...", "note": { "id": "{opaque_note_id}", "type": "note", "name": "my-note.md" } } ``` **Response fields:** | Field | Type | Description | |-------|------|-------------| | `content` | string | Sanitized markdown content | | `note` | object | Note resource (unpermissioned format) | **Error responses:** | HTTP Status | Code | Cause | |-------------|------|-------| | 406 | `1605 (Invalid Input)` | Node is not a note | | 404 | `1609 (Not Found)` | Quickshare not found, or note is in trash | | 450 | `1694 (Bandwidth Limit)` | Transfer limit reached | | 401 | `1680 (Access Denied)` | File access denied (virus/DMCA/locked) | #### Preview File ``` GET /current/quickshare/{opaque_id}/storage/preview/{preview_type}/read/ GET /current/quickshare/{opaque_id}/storage/preview/{preview_type}/read/file/{filename} ``` Preview types: `bin`, `thumbnail`, `image`, `hlsstream`, `pdf`, `spreadsheet`, `audio`, `mp4`. **Preview Limits:** Max 20x file size for total preview bytes. Multi-file previews (e.g., HLS) return a `307 Temporary Redirect` to a sub-file endpoint. #### QuickShare Error Responses | HTTP Status | Code | Message | Cause | |-------------|----------------------|------------------------------------------------------------------------|------------------------| | 406 | `1605 (Invalid Input)` | Validation error | Invalid opaque ID | | 404 | `1609 (Not Found)`| "Quickshare not found." | No such QuickShare | | 450 | `1694 (Bandwidth Limit)`| "You have exceeded the bandwidth policy for this Quickshare." | Transfer limit reached | | 401 | `1680 (Access Denied)` | "You have reached the maximum number of previews for this Quickshare." | Preview limit reached | | 422 | `1698 (Unprocessable Entity)` | "The source file is corrupt or unreadable and cannot be previewed." | Source is corrupt/truncated; do not retry | --- ## File Share (Durable Single-File Share) A **File Share** is the durable successor to QuickShare: a long-lived, link-shareable view of **one file** from a workspace. Unlike QuickShare it is durable by default (expiry is optional, never forced) and has no per-link transfer cap — bandwidth is metered to the owning organization like normal storage. A File Share is bound to a single workspace file node at creation; the binding is immutable (rebinding means creating a new File Share). **Addressing — opaque id.** A File Share is addressed by an **opaque public id** (the `id_alt` field): an unguessable, non-enumerable handle (the same id shape used for nodes and uploads), which appears in its share links. Every File Share route — public read and management alike — accepts this opaque id; the legacy numeric profile id is also accepted during the transition, so existing links keep working. Build links from `id_alt`. ### Access Tiers Set with `access_option` at create/update time: | Tier | Value | Who can open the link | |------|-------|-----------------------| | Anyone with link | `anyone_with_link` | Anyone, no account required (anonymous) | | Any registered | `any_registered` | Any authenticated Fastio user | | Named people | `named_people` | Only users explicitly granted access (the **default**) | On top of the tier, **per-user grants** raise an individual user's capability — `view`, `download`, or `edit`. An optional **link password** can gate any tier; it is supplied on the public read endpoints via the **`x-ve-password` request header**, never in the URL. **Workspace members (implicit access).** The shared file lives in the File Share's parent workspace, so an authenticated **member of that workspace** reaches it through the File Share exactly as they would through the workspace itself — they are admitted **regardless of tier or named grant**, at the capability their **workspace role** confers on the file (a workspace editor gets `edit`, a view-only member gets `view`). This is why the owner/creator and other workspace members can view — and collaboratively **edit notes on** — their own `named_people` share without granting themselves. A set **link password still applies** to members. Anonymous callers and non-members are governed solely by the tier + grant rules above. ### Public Read Endpoints These serve the link viewer. They are anonymous-allowed where the access tier permits (e.g. `anyone_with_link`); for `any_registered` / `named_people` the caller must present a bearer token and have sufficient access. A link password, if set, is presented via the `x-ve-password` header. #### File Share Details ``` GET /current/fileshare/{fileshare_id}/details/ ``` Returns viewer metadata and the bound file's full node detail under `file` — the same node shape the workspace file view returns. Metadata extracted from inside the file bytes (EXIF / media container) is included only when the caller holds `download` or higher. `comments_enabled` is the effective value for this view: always `false` on an `anyone_with_link` File Share. The `effective_capability` field is the single highest capability **the calling identity** actually holds on this File Share — `view`, `download`, or `edit` — resolved after the access tier, any per-user grant, and the link password are all applied. Use it to render the right controls up front (show the Download button only at `download` or higher; show "Upload new version" only at `edit`) instead of attempting an action and handling a later denial. Because the details response is only returned once the caller has passed the read gate, the value is present and at least `view` on the normal path; it is omitted only in the rare case the capability probe itself faults — treat an absent value as "unknown" and fall back to attempting the action. **Manage context (authenticated callers only).** The details response additionally carries an optional manage block that lets an owner's UI decide whether to surface in-place management controls without a separate lookup: - `can_manage` (boolean) — `true` when the **authenticated** caller may manage this File Share (a member of its parent workspace, the same requirement the management endpoints enforce), `false` when authenticated but not a manager. It is **omitted entirely for anonymous callers**, so the public link stays anonymous. - `workspace_id` (string) — the owning workspace id, returned **only when `can_manage` is `true`**. Pass it to the workspace-scoped management endpoints (list/update/grants/delete). A non-manager never receives it. - `creator_uid` (string) — the File Share creator's user id, returned **only when `can_manage` is `true`**. These fields are additive — anonymous viewers and clients that ignore them are unaffected. **Response (authenticated manager):** ```json { "result": true, "fileshare": { "fileshare": "1234567890123456789", "id_alt": "adheih5r326qjiqvk4wfamvt4rqeh", "title": "Quarterly Presentation", "access_option": "anyone_with_link", "has_password": false, "comments_enabled": false, "effective_capability": "download", "can_manage": true, "workspace_id": "9876543210987654321", "creator_uid": "1122334455667788990", "file": { "id": "2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4", "type": "file", "name": "presentation.pdf", "size": 5242880, "mimetype": "application/pdf" } } } ``` For an anonymous viewer the three manage fields are absent; for an authenticated non-manager only `can_manage: false` is present (no `workspace_id` / `creator_uid`). #### Download the Bound File ``` GET /current/fileshare/{fileshare_id}/storage/read/ ``` Returns raw binary content with `Content-Type` and `Content-Disposition` headers. Requires the `download` capability (the `view`-only level does not download). #### Preview the Bound File ``` GET /current/fileshare/{fileshare_id}/storage/preview/{preview_type}/read/ GET /current/fileshare/{fileshare_id}/storage/preview/{preview_type}/read/{download_token}/file/{filename} ``` Preview types match the storage preview surface (e.g. `thumbnail`, `image`, `pdf`, `mp4`, `hlsstream`). Requires the `view` capability. Multi-file previews (e.g. HLS) return a `307 Temporary Redirect` to a sub-file endpoint. #### Version History ``` GET /current/fileshare/{fileshare_id}/storage/versions/ GET /current/fileshare/{fileshare_id}/storage/versions/{version_id}/read/ ``` List the bound file's versions (`view` capability) and download a specific historical version (`download` capability). Version listing is read-only — there is no restore/promote on the public surface. ### Management Endpoints (Authenticated Workspace Member) Creating, listing, updating, deleting, and managing grants are documented in the **Workspaces** reference (`POST /current/workspace/{workspace_id}/create/fileshare/`, `GET /current/workspace/{workspace_id}/list/fileshares/`, and the `POST|PATCH /current/fileshare/{id}/update/`, `DELETE /current/fileshare/{id}/delete/`, `GET|POST|DELETE /current/fileshare/{id}/grants/` endpoints). All require the caller to be an authenticated **member of the File Share's parent workspace**. ### External Edit (Write-Back) A holder of an **`edit`** grant can replace the shared file's content without being a workspace member, by targeting the File Share id as the update target of a normal upload session. This is covered in the **Upload** reference (`action=update`, `instance_id={fileshare_id}`, `file_id={bound_node_id}`), including the optional `if_version_id` compare-and-swap precondition and how a version conflict is surfaced. ### Collaborative Notes (Realtime) When the bound file is a **note** (a `.md` text document), an authorized recipient can read and collaboratively edit it in real time rather than downloading and re-uploading binary content. Real-time edits from a File Share recipient and from members of the owning workspace converge on the **same live document** — there is no forking or separate copy. A note bound to a File Share cannot be replaced by a binary upload; its content changes only through the endpoints below or the real-time collaboration session. The flow is: mint a short-lived collaboration token with the **note-auth** endpoint, then send that token (not your session bearer token) as `Authorization: Bearer {auth_token}` to read the note's text and, if it carries edit standing, to push updates. The same token is what a client presents to join the real-time collaboration session. Full contract (responses, errors, the version-conflict shape): *File Share Note Endpoints* in the Storage reference. ``` GET /current/fileshare/{fileshare_id}/realtime/note-auth/{note_id}/ GET /current/fileshare/{fileshare_id}/storage/readnote/{note_id}/ POST /current/fileshare/{fileshare_id}/storage/updatenote/{note_id}/ ``` - **note-auth** — mints a 15-minute collaboration token for the bound note. The caller must be signed in and must pass the File Share's access gate, including the `x-ve-password` header when the link is password-protected. The token carries **edit** standing when the caller holds an `edit` grant on the File Share **or** is a workspace member whose role confers edit on the file (a credential scoped below edit on this File Share downgrades it); otherwise it is read-only. Anonymous anyone-with-link visitors cannot mint a token (they use the read-only download/preview surface above). ```json { "result": true, "expires_in": 900, "auth_token": "..." } ``` - **readnote** — returns the note's current text (`content`) and its node (`note`). Token-only: a session bearer token alone is refused with 401. Pass an optional `version_id` query parameter to read a historical version instead of the live content. - **updatenote** — replaces the note's text and/or renames it. Requires an **edit** token (a read-only token is refused with 403). Send `name` (must end in `.md`) and/or `content` as form fields in the request body; an empty or whitespace-only `content` is rejected as invalid input. Supply the optional `if_version_id` precondition for a compare-and-swap; on a version conflict the endpoint returns `409` naming the current version so the client can rebase and retry. ### Owner-Side Visibility (Comments) A File Share is a view of a file that **also lives in the owning workspace**, so comments left by File Share recipients are visible to workspace members on that same file — it's the same node, so the comments appear in both places. Specifically, for a workspace member: - **Activity feed** — File Share comment activity surfaces on the owning workspace's realtime / activity feed, so workspace members see when a recipient comments (the nudge carries no comment text). - **File comments** — the workspace node comment list for the bound file includes the File Share's comments alongside the workspace's own. They appear **read-only** in the workspace view (`can_reply` / `can_edit` / `can_delete` are `false`) — replies and edits to a File Share comment go through the File Share's own surface. - **Workspace search** — the workspace `comments` search bucket includes comments left through the workspace's **live** (active, non-expired, non-revoked) File Shares. This visibility is **one-directional: workspace ⊇ File Share.** Workspace members see File Share comments, but File Share recipients **never** see the workspace's internal comments — a recipient's view stays isolated to their own File Share's comment thread. ### File Share Error Responses | HTTP Status | Code | Cause | |-------------|------|-------| | 406 | `1605 (Invalid Input)` | Invalid id or input value | | 401 | `1650 (Authentication Invalid)` | A link password is required and was missing or wrong (present it via the `x-ve-password` header) | | 403 | `1700 (Forbidden)` | The access tier or named grant does not permit the caller | | 404 | `1609 (Not Found)` | No such File Share, the bound file content is no longer available, or the owning organization no longer has an active plan (the link stops working when the plan ends, and the response is the same as for a link that never existed) | --- ## Share Storage Share storage follows the same API patterns as workspace storage. See `storage.txt` for full endpoint documentation. Replace `/workspace/{workspace_id}` with `/share/{share_id}` in all storage paths. Available operations: - `addfile` -- add file from upload - `createfolder` -- create folder - `list` -- list folder contents (keyset pagination) - `details` -- node details - `update` -- rename, replace content - `move` -- move within share - `copy` -- copy within share - `transfer` -- copy to another storage instance - `delete` -- move to trash - `purge` -- permanently delete from trash - `restore` -- restore from trash - `restore-version` -- restore previous version - `versions` -- list version history - `read` -- download file - `requestread` -- get download token - `zip` -- download folder as ZIP (token-based access supported) - `requestzip` -- get ZIP download token - `search` -- keyword search - `preview` -- file previews (preauthorize, read, token-based read) - `transform` -- image transforms (status, request, read, requestread) - `requestpreview` -- one-time preview nonce (shares with `download_security=medium` only) - `readnote` -- read note content as JSON (token-based access supported) - `lock` -- file locking (acquire, heartbeat, release, status, override) - `content` -- read a file's or note's extracted text as ordered chunks - `inventory` -- enumerate every live file (`/storage/inventory/`) - `recent` -- recently changed files (`/storage/recent/`) **Not available in shares:** `addlink`, `createnote`, `updatenote`, `quickshare`