Organization Management Org CRUD, members, billing, discovery

Base URL: https://api.fast.io/current/ Auth: Bearer {jwt_token}

An organization (org) is a collector of workspaces. It can represent a company, a business unit, a team, or simply a personal collection. Orgs are the billable entity — storage, credits, and member limits are tracked at the org level. Every workspace and share lives under an org.

Profile IDs are 19-digit numeric strings. Most endpoints also accept the org's domain name (e.g., acme) in place of the numeric ID.


Internal vs External Orgs

Agents must call both GET /current/orgs/list/ and GET /current/orgs/list/external/ to discover all orgs they can access.

External orgs are the most common pattern when a human invites an agent to help with a specific project — they add the agent to a workspace but not to the org itself.

If the human later invites the agent to the org itself, it moves from external to internal and gains org-level access.


Org Field Constraints

FieldTypeMinMaxRegex / RulesDefault
domainstring263^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$ Lowercase alphanumeric + hyphens. Must be unique. Must not be reserved.Required
namestring3100Free text display namenull
descriptionstring101000Free textnull
industrystring——Must be one of the values from GET /current/orgs/industries/null
perm_member_managestring——'Member or above', 'Admin or above', 'Owner only''Member or above'
perm_workspace_createstring——'Member or above', 'Admin or above', 'Only Org Owners'. Enterprise plan only to tighten (raising the minimum role); lowering it or resubmitting it unchanged works on any plan.'Member or above'
workspace_create_allowliststring (JSON list of user IDs, sent in this one field)—500User IDs allowed to create workspaces regardless of perm_workspace_create. Every ID must be a current member of the org. Enterprise plan only to tighten (removing a current member from the list); adding users or resubmitting it unchanged works on any plan. Returned as an array.[]
sharing_sharesboolean——Whether members may create shares (Send / Receive / Exchange, including shared folders). Enterprise plan only to tighten (switching it off).true
sharing_file_linksboolean——Whether members may create single-file share links. Enterprise plan only to tighten (switching it off).true
perm_authorized_domainsstring——Email domain for auto-join (e.g., acme.com)null
billing_emailstring (email)——Valid email with reachable domainUser's email
accent_colorstring (JSON)——JSON-encoded color object {"color":"#RRGGBB","opacity":0-100} (both keys required)null
background_colorstring (JSON)——JSON-encoded color object {"color":"#RRGGBB","opacity":0-100} (both keys required)null
background_modestring——One of the supported background display modesnull

Member Roles and Permissions

RoleLevelCan manage membersCan manage settingsCan manage billingCan close orgCan transfer ownership
OwnerHighestYesYesYesYesYes
AdminHighYes (if perm_member_manage allows)YesYesNoNo
MemberStandardIf perm_member_manage = 'Member or above'NoNoNoNo
ViewLowestNoNoNoNoNo

The perm_member_manage org setting controls the minimum role required to add, remove, or update members.


Organization Security Controls

Organizations on the Enterprise plan can restrict what their members may create. All four settings are read and written on the org (see Org Field Constraints above); they are returned to admins on GET /current/org/{org_id}/details/ and written with POST /current/org/{org_id}/update/.

SettingEffect
perm_workspace_createMinimum role required to create a workspace in the org.
workspace_create_allowlistNamed users who may create a workspace regardless of the role threshold. The two are combined with OR.
sharing_sharesWhen false, members may not create new shares (Send / Receive / Exchange, including shared folders).
sharing_file_linksWhen false, members may not create new single-file share links.

Both sharing settings also exist on each workspace. The effective answer is the org setting AND the workspace setting, so the org acts as a ceiling: a workspace can switch sharing off for itself, but cannot switch it back on when the org has switched it off.

Turning a sharing setting off blocks new creation only. Shares and links that already exist keep working, and remain editable and deletable.

Reading the effective answer. Do not infer these from the raw settings — a member cannot read them. Read capabilities on GET /current/org/{org_id}/details/ (for workspace creation) and on GET /current/workspace/{workspace_id}/details/ (for sharing), which already combine the role, the plan, and both policy levels for the calling user.

Refusals. A create request that policy forbids returns HTTP 403 with a reason in params: policy_workspace_create_denied or policy_sharing_disabled. These are distinct from a plan-limit refusal, which keeps reporting its own error.

The Enterprise gate is directional. Only a write that tightens one of these four settings needs the Enterprise plan: switching a sharing setting off, raising the role required to create a workspace, or taking a user off the allowlist. Such a write from an organization without the plan returns HTTP 403 with params.reason = plan_required. Resubmitting a setting at the value it already holds, or relaxing one — switching sharing back on, lowering the role, adding a user to the allowlist — returns 200 on any plan, so an organization that changes plan can always unwind what it configured. Reading is never gated.

Audit. A write that changes one of these four settings adds a policy_changes map to the org_updated event: policy_changes.<key> = { before, after } for perm_workspace_create, sharing_shares and sharing_file_links (before is the value that was in force, so a setting never configured reports its default), and policy_changes.workspace_create_allowlist = { added, removed } — counts, not user IDs, because the event is visible to every member while the list itself is admin-only. policy_changes is absent when no setting changed.

Organizations that have never configured these settings behave exactly as they did before they existed: workspace creation is open to members and above, and both sharing settings are on.


Collaboration Policies (External Invites)

Organizations on the Enterprise plan can restrict which members may bring outside people onto Portals, Shared Folders, File Shares and Workspaces. Three org-level settings share one policy envelope shape:

SettingGoverns
external_invites_sharesShared Folders, and File Share grants.
external_invites_portalsPortals.
external_invites_workspacesWorkspaces.

The envelope. Each setting is a JSON object with two role baselines and an optional per-member exception list:

{ "admin": "allowed", "member": "denied", "overrides": { "9876543210987654321": "allowed" } }

Reading and writing.

Enforcement. The org policy is the first half of the answer; each Portal, Shared Folder and Workspace also carries its own external_invites flag (allowed / denied, absent = inherit the org policy — see the Shares and Workspaces references); a File Share has no flag of its own and follows the org policy only. An object can only ever tighten below the org result, never loosen it. Refusals are HTTP 403 with a params.reason distinguishing which layer denied, so the client can route the user to the right admin:

ReasonMeaning
external_invites_deniedThe org's policy denies this inviter.
external_invites_object_deniedThe object's own external_invites flag denies it — the remedy is that object's admin, not the org admin.
plan_requiredA write tightening one of the three org settings, or credential_policy, was sent by an org without the Enterprise plan (same directional gate as Organization Security Controls above).

The check looks only at whether the invitee is external — a current org member, or someone on one of this org's verified SSO domains, is never refused regardless of the policy. It applies wherever external access is created or widened: sending or resending a share, portal, workspace or File Share invitation; adding an outside user directly; accepting a pending invitation (a refusal at acceptance leaves the invitation pending rather than failing it, since the policy or the inviter's own standing can change between issuance and acceptance); and creating a Portal, Shared Folder or File Share with public access, or widening an existing one's public-access setting, including 'Anyone with a registered account' to 'Anyone with the link'. Inviting a member to the org itself is never gated — an org invite is definitionally of a non-member.

POST /current/user/invitations/acceptall/ is partial-success under this policy: a policy-refused invitation is skipped (left pending, no access granted) while every other pending invitation in the batch is still processed — see that endpoint in the Auth reference for the response shape.

Audit. org_updated carries the three keys inside its existing policy_changes map: policy_changes.<key> = { before: {admin, member}, after: {admin, member}, overrides: {added, removed, changed} }. The override numbers are counts, not user IDs — policy_changes renders for every member, while the exception list itself stays admin-only.


Credential Policy

Organizations on the Enterprise plan can cap what an API key or an OAuth grant issued inside the org may hold, and constrain it on every later request, not only at the moment it is issued. One org-level setting, credential_policy, governs two credential families independently:

FamilyGoverns
api_keysEvery API key issued by a member of this org.
oauthEvery OAuth authorization granted by a member of this org.

The envelope, twice. credential_policy wraps one ordinary policy envelope (see Collaboration Policies above for the {admin, member, overrides} shape) per family — but here each role's value is itself an object, not a bare string:

{
  "api_keys": {
    "admin": { "max_mode": "rwa", "scope_types": ["org", "workspace", "share", "fileshare"] },
    "member": { "max_mode": "rw", "scope_types": ["workspace", "share"] },
    "overrides": { "9876543210987654321": { "max_mode": "r", "scope_types": ["share"] } }
  },
  "oauth": {
    "admin": { "max_mode": "rwa", "scope_types": ["org", "workspace", "share", "fileshare"] },
    "member": { "max_mode": "rw", "scope_types": ["workspace", "share"] },
    "overrides": {}
  }
}

Reading and writing.

Enforcement.

Refusals are 403 with params.reason:

ReasonMeaning
credential_policy_modeThe mode the credential holds for this entity exceeds the org's max_mode. This inverts the ordinary scope error, where the held mode is too low.
credential_policy_scopeThe credential names an entity type this family's scope_types does not allow.
credential_policy_ssoThe credential owner's account is on this org's SSO-enforcing domain and is not exempt — see Credential-request enforcement in the SSO reference.

Audit. org_updated carries credential_policy inside its existing policy_changes map, one entry per family: policy_changes.credential_policy = { api_keys: {before, after, overrides: {added, removed, changed}}, oauth: {before, after, overrides: {added, removed, changed}} }.


Cloud Sync Policy

Organizations on the Enterprise plan can restrict whether cloud-sync import runs at all, and whether it may write local changes back to the connected provider. One org-level setting, cloud_sync, is a policy envelope (see Collaboration Policies above for the {admin, member, overrides} frame) whose value is:

{ "enabled": true, "mode": "read_write" }

Reading and writing.

Refusals are 403 with params.reason:

ReasonRaised at
cloud_sync_disabledIdentity provision, source create, each provider's OAuth-complete endpoint, and the manual write-back actions (push-writeback, retry-writeback, a keep_local resolve-conflict), while enabled is false somewhere in the org-then-workspace chain.
cloud_sync_read_onlyA manual write-back action (push-writeback, retry-writeback, a keep_local resolve-conflict) while the chain meets at mode: "read".

mode never gates opening a new connection or inspecting/disconnecting an existing one — only enabled does, and only write-back actions consult mode. When the policy cannot be read at all, the same endpoints answer 1693 (Temporarily Unavailable) → 503 instead — retryable, never a denial.

Audit. org_updated carries cloud_sync inside its existing policy_changes map: policy_changes.cloud_sync = { before: {admin, member}, after: {admin, member}, overrides: {added, removed, changed} } — each baseline is its {enabled, mode} value (null when unconfigured), and overrides are only counted, never named.


Require-2FA Policy

Organizations on the Enterprise plan can require a second factor to sign in. One org-level setting, auth_require_2fa, is a policy envelope (see Collaboration Policies above for the {admin, member, overrides} frame) whose per-role value is one of two words:

{ "admin": "required", "member": "optional", "overrides": { "9876543210987654321": "required" } }

Scope — password and social login only. This policy governs the moment a session is minted by those two flows, because they are the only ones where a factor can be demanded at that moment. It has no effect on API keys, OAuth grants, or MCP tokens — none of them is challenged for a factor at request time, regardless of what this policy says. An SSO-minted session is compliant regardless of this or any other org's requirement — the identity provider owns that factor, and the enterprise SSO exchange response never carries a 2factor or enrol_required field. See Interactive Login & Enrolment in the Auth reference for the enrolment flow this policy drives.

Reading and writing.

Refusal on write — HTTP 403 with params.reason = two_factor_admin_unenrolled: the endpoint refuses ONLY when the writer's own effective requirement goes optional/unconfigured → required and the writer holds no factor themselves (self-lockout). Tightening the member baseline, or setting an override for someone else, always succeeds even from an unenrolled admin — there is no blanket “enrol before you may require it” rule, and no SSO escape hatch; enrolling is the only remedy for the writer's own lockout.

Two transient refusals, and on both of them NOTHING WAS SAVED. Beside the 403 above, a write of this key can answer 503 twice over, and both are retryable:

Side effects of flipping this policy on. The sessions of users who become required and hold no factor are revoked, but asynchronously — there is a window between the policy write returning success and the sweep reaching that user's sessions. Two populations are deliberately left alone by this cut, and are instead challenged at their next login rather than swept immediately:

Removing an optional override is NOT in that group — it IS swept. Dropping an exception that leaves the user under a required baseline is a direct tightening write, and it queues a sweep targeted at exactly that one user rather than the whole org. The rule is: a write that tightens a baseline sweeps org-wide; a write that newly requires a factor of exactly one named user sweeps only them; anything ambiguous falls back to the org-wide sweep.

Audit. org_updated carries auth_require_2fa inside its existing policy_changes map: policy_changes.auth_require_2fa = { before: {admin, member}, after: {admin, member}, overrides: {added, removed, changed} } — the same shape as the collaboration policies above.


Access Policy (Geo / IP Restrictions)

Enterprise plan. An org can restrict which countries and/or IP ranges may reach its content at all — every workspace, share, upload, and read, across every credential type (browser session, API key, OAuth, MCP). Read and write it through the access_policy field on GET /current/org/{org_id}/details/ (admin-only, raw) and POST /current/org/{org_id}/update/ — see the field description under Update Organization above.

Value shape, evaluated separately per role:

{"admin":  {"countries": {"mode": "allow", "codes": ["US", "CA"]}, "ips": ["203.0.113.0/24"]},
 "member": {"countries": {"mode": "allow", "codes": ["US"]}, "ips": null},
 "overrides": {"9876543210987654321": {"countries": null, "ips": null}}}

Who is evaluated: owners and admins read the admin value; members and guests (non-org participants) read member; a per-user entry in overrides wins over either baseline. API keys, OAuth grants and MCP tokens resolve as the user they belong to. A storage download token or preview link resolves as the user who requested it (an older token predating this field reads the member baseline).

Evaluation, for the caller's IP and country:

  1. Pass if ips is set and the IP falls inside any listed range — this bypasses the country rule entirely.
  2. Otherwise pass if countries is set and the country passes it: in allow mode the country (or XX/T1) must be listed; in deny mode any country not listed passes, and an unknown/Tor caller passes unless XX/T1 is explicitly listed.
  3. Otherwise pass if both countries and ips are null (unrestricted).
  4. Otherwise blocked. An ips-only rule therefore blocks every IP not on the list, regardless of country. A request with no resolvable IP is always blocked by any restricting rule.

Tightening gate. A write that tightens access_policy — the new value blocks some IP or country the old value allowed — needs the Enterprise plan and is refused with 403 plan_required on a lower plan. Relaxing is always allowed, so a downgraded org can still undo its own restriction.

Self-lockout guard. A write that would take the caller's own current IP/country from pass to block is refused with 403 access_policy_self_lockout (params.ip / params.country). There is no override flag — add your own IP, or an override naming yourself, in the same write. If the result cannot be resolved, the write is refused with 503 access_policy_unavailable (retryable; nothing was saved).

Enforcement. Every org-content request from a blocked caller — any credential type — is refused with 403 geo_restricted (never 401, so a client must never treat this as a sign-out): params: {reason: "geo_restricted", rule: "country"|"ip", org_id, domain} (rule is "ip" for an IP-only rule, or for a missing/unresolvable client IP). The restriction list itself is never echoed to a blocked caller. Suggested copy: “Access to this organization is not permitted from your current location or network.” This includes upload endpoints for an org-governed target — create, chunk, stream, complete, and both upload/{id}/details and web_upload/{id}/details — which refuse a blocked caller with geo_restricted, never a 404; a 404 from a details read still means the session itself is gone, not that it was hidden by this policy. If an upload session's own target cannot itself be read to determine the policy, the call fails 503 access_policy_unavailable (retryable) rather than passing through unchecked. websocket/auth and activity/poll for an org-owned profile (see Activity Polling / WebSocket in the Events reference) return this same structured geo_restricted refusal ahead of any generic invalid-input error, and an MCP-classified request reaching any of these gets 403 mcp_access_denied instead — see AI, Intelligence & MCP Access Policy below. A missing owning org (rare) reports 403 access_policy_org; an unreadable stored policy or membership reports 503 access_policy_unavailable (retryable) — except that an unreadable stored access_policy resolves to blocked for everyone but the owner, so a corrupted policy fails closed rather than open.

Exemptions. Anonymous public access, and a signed-in user reading a share or file link whose access is set to “Anyone with the link” (public means public), are not geo-checked. Anything on that same share that needs actual membership — a write, or any member-only action — is checked. Internal pipeline and platform-agent (Ripley) tokens are also exempt.

Owner break-glass. The org owner always reaches GET org/{org_id}/details/ and POST org/{org_id}/update/, even from a blocked location — otherwise a misconfigured policy could lock an owner out of fixing it. Every other org call the owner makes is geo-checked the same as anyone else; a client can infer “recovery mode” when other org calls return geo_restricted while details keeps succeeding. An event is recorded only when that access actually bypassed a block, and is rate-limited.

Cross-org lists. A row belonging to an org that blocks the caller — by this policy, or by the MCP access policy on an MCP request (see below) — is dropped before paging from orgs/list (including the org's own row), orgs/all, orgs/list/external, workspace and share list/available endpoints, the caller's own share list (GET user/me/list/shares/ — see the Shares reference), upload lists, and events/search without a workspace_id filter. Most of these are dropped before paging so pages and totals stay exact; the one exception is the web_upload list, which is paged in SQL, so a page can come back short of limit while total still counts every row. user/available_profiles is not filtered — it reports booleans about the caller's own relationships, with no org rows to drop.

Known limits. An already-open WebSocket connection is not forcibly closed when a policy changes — it is re-checked the next time its token is minted (on reconnect). A storage download token issued before this feature shipped is checked against the member rule for the remainder of its (short) life. Access through the Fastio MCP is evaluated at the MCP worker's own network egress, which does not necessarily reflect the end user's real location.


AI, Intelligence & MCP Access Policy

Enterprise plan. Alongside access_policy above, an org can independently gate five AI/MCP surfaces and one workspace allowlist, all written through POST /current/org/{org_id}/update/ — see the ai_agent / ai_intelligence / ai_metadata / ai_summaries / mcp_access / ai_workspaces field descriptions under Update Organization above, and the raw/capabilities echo fields under Get Org Details. Unconfigured means allowed for every one of these keys.

FieldGoverns
ai_agentRipley Agent chat (create / send / publish / rename), AI share generation, share auto-title and AI OG image, events summarize, dashboard AI
ai_intelligenceTurning a workspace's/share's intelligence on, the meaning-based (semantic) search leg, background indexing, and Ripley Agent retrieving from a workspace's index
ai_metadataMetadata extraction (single-file, per-folder, compound search) and automatic extraction on ingest
ai_summariesPer-file AI summaries at ingest — background only, never an interactive refusal. Denying it also stops new files from being indexed, since indexing needs a file's summary first; existing index data is untouched
mcp_accessAny request classified as coming from the Fastio MCP
ai_workspacesAn allowlist narrowing which workspaces Intelligence/metadata background processing (and interactive Intelligence/metadata) applies to; null = every workspace, [] = none

Refusals (branch on params.reason, never on error.code or HTTP status alone):

ReasonHTTPMeaning
ai_policy_denied403params.feature = agent|intelligence|metadata — the caller's resolved policy denies that feature.
ai_policy_workspace_not_allowed403params.feature, params.workspace_id — the feature is allowed, but this workspace is not on the ai_workspaces allowlist.
mcp_access_denied403params.org — the request was classified as MCP and this org denies MCP access. Cross-org lists drop the org's rows the same way for an MCP request. Account-level (user/*) endpoints are never blocked.

See the AI reference for exactly which endpoints return these (chat create/send/publish/rename, AI share, share auto-title/OG, metadata extraction, and the search endpoints' semantic leg), and Workspaces (ai_policy_state, can_use_ai_agent) for how a workspace's own details surface the effective, policy-aware answer per feature.


Compact Responses (output=)

Every endpoint that returns one or more org 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.

LevelFields returned on each org (cumulative)
terseid, domain, name, logo
standardterse + description, plan, user_permission (member-only), user_status, member, closed (member-only), locked (member-only), suspended (member-only), created (member-only), updated (member-only), parent (member-only), capabilities, accent_color, background_mode, background, use_background, background_color, homepage, subscriber (member-only), subscriber_cancel (admin-only), subscriber_trial_until (member-only), payment_state (member-only), payment_failed_at (member-only), access_ends_at (member-only)
fullstandard + subscriber_trial_credits, billing_email, social links (facebook, instagram, twitter, youtube), encryption_key, perm_* blocks (including perm_auth_domains, perm_member_manage, perm_workspace_create), workspace_create_allowlist, sharing_shares, sharing_file_links, external_invites_shares/_portals/_workspaces (admin-only), dmca, owner_defined, platform, storage

Use terse for org switchers and billing-entity pickers — it includes the ID, URL domain, display name, and logo so the org-switcher sidebar can render entries without falling back to initials. Use standard for org list views, most member-facing dashboards, and branding-aware surfaces — it adds plan, description, lifecycle flags (including the locked and suspended lifecycle/billing chips, emitted to members only), the caller's permission and status, timestamps, hierarchy pointer, plan-gated capabilities, the visual-identity bundle (accent color, background, homepage), and the subscription state fields (subscriber, subscriber_cancel, subscriber_trial_until, payment_state, payment_failed_at, access_ends_at) that list-view subscription chips render. Note that subscriber reports entitlement, not payment health — it stays true while a payment is failing — so read payment_state when you need to know whether billing is actually current. Use full (or omit the parameter) for the org settings screen, billing portal, auth-domain configuration, and any workflow that reads remaining trial credit, permission blocks, social links, or encryption metadata. Unknown tokens are silently ignored. Add the markdown modifier (e.g. ?output=standard,markdown) to receive the response as GitHub-flavored Markdown (Content-Type: text/markdown; charset=UTF-8) instead of JSON — see the cross-cutting ?output= reference for the full contract.

The capabilities object (member-visible) reports plan-gated features the org currently has access to, as booleans. It includes capabilities.signing — whether the org's plan and feature flags currently grant the e-signature surface (e-signature is enabled on every plan, so this is normally true). Read it to decide whether to surface signing UI rather than inferring availability from a denied request.

On GET /current/org/{org_id}/details/ only, capabilities additionally carries can_create_workspace (the effective answer for the calling user, combining their role, the plan, and the org's workspace-create policy), sso, org_controls, and external_invites_shares / external_invites_portals / external_invites_workspaces (see Collaboration Policies). Those are deliberately absent from org list responses, where the answer is not caller-specific enough to be useful; request the org's details when you need them.


Organization CRUD

Create Organization

POST /current/org/create/

Auth required. Creates a new organization. The authenticated user becomes the owner.

Request parameters

NameTypeRequiredDescription
domainstringYes2-63 chars, lowercase alphanumeric + hyphens, must be unique and not reserved. Used as the org identifier in URLs.
namestringNo3-100 chars. Display name for the org.
descriptionstringNoOrganization description.
industrystringNoIndustry type from predefined list (see GET /current/orgs/industries/).
accent_colorstring (JSON)NoBrand accent color as a JSON color object, {"color":"#RRGGBB","opacity":0-100}.
background_colorstring (JSON)NoBackground color as a JSON color object, {"color":"#RRGGBB","opacity":0-100}.
background_modestringNoBackground display mode.
facebook_urlstring (URL)NoFacebook page URL. Must be valid URL.
twitter_urlstring (URL)NoTwitter profile URL. Must be valid URL.
instagram_urlstring (URL)NoInstagram profile URL. Must be valid URL.
youtube_urlstring (URL)NoYouTube channel URL. Must be valid URL.
homepage_urlstring (URL)NoOrganization website URL. Must be valid URL.
perm_member_managestringNoWho can manage members. See Org Field Constraints above.
perm_authorized_domainsstringNoAuthorized email domain for auto-join.
billing_emailstring (email)NoBilling contact email. Defaults to user's email.

A newly created organization must select a paid plan (Starter, Business, or Enterprise) 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. This applies to agent and human accounts alike: agent accounts are ordinary accounts tagged account_type=agent and follow the same paid-plan flow. The legacy free plan is no longer available for new organizations.

New orgs on a monthly plan begin a free trial of up to 30 days, or until the included trial credits are used. Annual plans have no trial — an annual subscription is billed for the full term at signup. A trial also carries a credit allowance (trial_credit_limit), which differs by plan — read each plan's own trial_credit_limit. Reaching it ends the trial early and starts the subscription — so the trial is bounded by usage as well as by time, and a customer who consumes their allowance in three days is billed on day three. The exception is a trial with a cancellation scheduled: it never converts and is never charged; usage is held at the trial's credit allowance instead. Show both limits at checkout. A trial is only available on a user's first organization — if the user owns, or has ever owned, any other organization (including one they later closed), no trial is offered on a later org, no matter how much time has passed. A user is also only ever offered one free trial, ever — once a user has started a free trial on any organization, no later organization is offered another one, no matter how much time has passed. When blocked by either rule, a new org subscribes with immediate payment instead of a trial.

Read the trial length from the plan itself rather than assuming a fixed number of days: each plan reports free_days in its pricing, and free_days: 0 means the plan has no trial and payment is due at checkout.

curl example

curl -X POST "https://api.fast.io/current/org/create/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "domain=acme-corp" \
  -d "name=Acme Corporation" \
  -d "industry=technology"

Response (200 OK) — trial available

{
  "result": true,
  "org": {
    "id": "1234567890123456789",
    "domain": "acme-corp",
    "name": "Acme Corporation",
    "description": null,
    "logo": null,
    "accent_color": null,
    "closed": false,
    "suspended": false
  },
  "has_free_trial": true,
  "requires_payment": true,
  "is_agent": false
}

When the owner already owns, or has ever owned, another organization, no trial is offered — the org still subscribes but bills immediately. This block is permanent:

{
  "result": true,
  "org": { "...": "..." },
  "has_free_trial": false,
  "requires_payment": true,
  "is_agent": false,
  "no_trial_reason": "Free trials are only available on your first organization."
}

A trial is also blocked once the owner has ever started a free trial on any organization — this is also permanent, no matter how much time has passed:

{
  "result": true,
  "org": { "...": "..." },
  "has_free_trial": false,
  "requires_payment": true,
  "is_agent": false,
  "no_trial_reason": "A free trial has already been used on this account."
}

Both blocks are permanent, so trial_available_at is never returned in any case.

requires_payment is always true for new orgs (there is no free plan). Until a paid plan is selected the org is in an upgrade-only state (gated endpoints return 402). This applies to all accounts, including agent accounts (is_agent: true).

Response fields

FieldTypeDescription
orgobjectOrganization resource object
org.idstring19-digit numeric organization ID
org.domainstringURL-safe subdomain
org.namestring/nullDisplay name
org.descriptionstring/nullDescription
org.logostring/nullLogo asset URL
org.accent_colorobject/nullBrand color ({color, opacity})
org.closedbooleanWhether org is closed
org.suspendedbooleanWhether org is suspended
has_free_trialbooleanWhether a trial is available at checkout. true only when this is the owner's first organization and the owner has never started a free trial before; false for any org after the first, or once the owner has ever started a free trial (both permanent).
requires_paymentbooleanWhether a paid plan is required before the org can be used. true for new orgs.
is_agentbooleanWhether the creating user is an agent account
no_trial_reasonstringHuman-readable reason a trial is unavailable (only when has_free_trial is false)

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. Five- and six-digit codes (and the 9661-9669 family) are real 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.

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"An invalid org domain was supplied."Invalid domain format
1605 (Invalid Input)406"The supplied org domain name is restricted."Domain is reserved
1605 (Invalid Input)406"The supplied org domain name is already in use."Domain already taken
1605 (Invalid Input)406"An invalid configuration was supplied..."Metadata validation failed
1605 (Invalid Input)406"Invalid JSON provided for {key}."Malformed JSON in color fields
1663 (Update Failed)500"There was an internal error processing your create request."Org creation failed
1654 (Internal Error)500"There was an internal error processing your create request."Internal error during creation
1654 (Internal Error)500"We were unable to create your organization..."Internal error
1697 (Geo Restricted)452Geo restriction messageRequest blocked by the geo check
1680 (Access Denied)401"Access restricted due to security concerns."Request blocked by the risk check (no geo block)

Get Org Details

GET /current/org/{org_id}/details/

Auth required. Returns full org details. Fields vary by the requesting user's permission level.

{org_id} accepts a 19-digit numeric ID or the org's domain name.

Access levels

RoleAccessNotes
OwnerFull accessFull access to all organization settings and security configuration
AdminExtended accessIncludes billing info, permissions, subscriber status, credit balance
MemberStandard accessBasic org info, plan, subscriber status (boolean only — no credit balance)
ViewLimited accessPublic fields only

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/details/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "org": {
    "id": "1234567890123456789",
    "domain": "acme-corp",
    "name": "Acme Corporation",
    "description": "Leading provider of innovation",
    "logo": "https://assets.fast.io/org/logo.png",
    "accent_color": {"color": "#0066CC", "opacity": 100},
    "closed": false,
    "locked": false,
    "suspended": false,
    "created": "2024-01-15 10:30:00 UTC",
    "updated": "2024-06-20 14:45:00 UTC"
  }
}

Response fields

FieldTypeDescription
org.idstring19-digit numeric organization ID
org.domainstringURL-safe subdomain
org.namestring/nullDisplay name
org.descriptionstring/nullDescription
org.logostring/nullLogo asset URL
org.accent_colorobject/nullBrand color ({color, opacity})
org.closedbooleanWhether org is closed
org.lockedbooleanWhether org is locked
org.suspendedbooleanWhether org is suspended
org.createdstringCreation timestamp
org.updatedstringLast update timestamp
org.planstringBilling plan identifier (e.g., "starter_monthly", "business_v3_monthly", "enterprise_v2_monthly").
org.subscriberbooleanWhether the org has an active subscription (for an org without a paid plan, reflects whether its credit allowance is available). Member+ only.
org.subscriber_trial_untilinteger/nullUnix timestamp when the trial period ends. null for paid plans or if no trial. Member+ only.
org.payment_statestringPayment health: current, past_due, or unpaid. unpaid is terminal — retries have stopped. Read this rather than subscriber to tell whether billing is current: subscriber stays true throughout a failed payment. Unrecognised states report current. Member+ only.
org.payment_failed_atstring/nullStart of the billing period whose payment failed, Y-m-d H:i:s UTC. null unless payment_state is past_due or unpaid. Read this as “the period that is unpaid”, NOT as the instant the charge was declined. For a cycle-renewal failure the two coincide, because a past-due subscription's period does not advance while its invoice is unpaid. For a failure on a mid-cycle invoice — a renewal on a subscription with no pending plan change, for instance — the period start can be materially earlier than the failure. (On an account that is current on its billing, an upgrade that needs additional card authentication does not reach past_due: the plan change stays pending on the current plan until authentication completes, and only then does the plan change — see payment_recovery under Create or Update Subscription below. An account already behind on payment, or an older subscription still on the prior billing mechanics, may still become past-due.) Do not render it as “payment failed on {date}”. A formatted string, not a Unix timestamp — unlike subscriber_trial_until above. Member+ only.
org.access_ends_atnullReserved. Always null: the date access ends is not currently knowable, so none is reported rather than an estimate. Member+ only.
org.subscriber_trial_creditsinteger/nullCredits remaining in the current billing period. Admin+ only.
org.perm_workspace_createstringMinimum role required to create a workspace. Admin+ only.
org.workspace_create_allowlistarray of stringUser IDs allowed to create a workspace regardless of the threshold. Admin+ only.
org.sharing_sharesbooleanWhether members may create shares. Admin+ only.
org.sharing_file_linksbooleanWhether members may create single-file share links. Admin+ only.
org.external_invites_sharesobject/string/nullRaw collaboration-policy envelope governing Shared Folder and File Share invitations — null when unconfigured, an object ({admin, member, overrides}) when readable, or the raw stored string when unreadable (repair by resaving). Admin+ only. See Collaboration Policies.
org.external_invites_portalsobject/string/nullSame shape, governing Portal invitations. Admin+ only.
org.external_invites_workspacesobject/string/nullSame shape, governing Workspace invitations. Admin+ only.
org.credential_policyobject/string/nullRaw caps on the API keys and OAuth grants issued in this org — null when unconfigured, an object ({api_keys, oauth}) when readable, or the raw stored string when unreadable. Admin+ only. See Credential Policy.
org.cloud_syncobject/string/nullRaw cloud-sync policy envelope — null when unconfigured, an object ({admin, member, overrides}, each value {enabled, mode}) when readable, or the raw stored string when unreadable (repair by resaving). Admin+ only. No capabilities twin — the effective answer is the workspace's effective_cloud_sync. See Cloud Sync Policy.
org.auth_require_2faobject/string/nullRaw policy envelope requiring a second factor at password/social login — null when unconfigured, an object ({admin, member, overrides}, values "required"/"optional") when readable, or the raw stored string when unreadable. Admin+ only. No capabilities twin — see Require-2FA Policy.
org.access_policyobject/string/nullRaw geo/IP access-restriction envelope — null when unconfigured, an object ({admin, member, overrides}) when readable, or the raw stored string when unreadable (repair by resaving). Admin-only. The restriction list itself is never echoed to anyone but an admin. See Access Policy (Geo / IP Restrictions).
org.ai_agent / org.ai_intelligence / org.ai_metadata / org.ai_summaries / org.mcp_accessobject/string/nullRaw policy envelopes ({admin, member, overrides}) for each AI/MCP surface — null when unconfigured, an object when readable, or the raw stored string when unreadable. Admin-only. See AI, Intelligence & MCP Access Policy.
org.ai_workspacesarray/string/nullRaw Intelligence/metadata workspace allowlist — null means every workspace, an array of workspace id strings is the allowlist ([] means none), or the raw stored string when unreadable. Admin-only.
org.security_alertsobject/string/nullRaw security-alerts envelope — null means the defaults (every alert on, auditors included), an object ({enabled: [...], include_auditors}) when readable, or the raw stored string when unreadable. Admin-only. See Security Alerts below.
org.capabilities.can_create_workspacebooleanWhether the calling user may create a workspace in this org right now — role, plan and policy combined. Member+ only, details responses only.
org.capabilities.ssobooleanWhether the org's plan includes identity-provider configuration. Member+ only, details responses only.
org.capabilities.org_controlsbooleanWhether the org's plan includes the security controls above. Member+ only, details responses only.
org.capabilities.external_invites_sharesbooleanWhether the calling user may currently invite an outsider to a Shared Folder or File Share in this org. Member+ only, details responses only.
org.capabilities.external_invites_portalsbooleanSame, for Portals. Member+ only, details responses only.
org.capabilities.external_invites_workspacesbooleanSame, for Workspaces. Member+ only, details responses only.
org.capabilities.credential_policy_api_keysobjectThe calling user's own effective {max_mode, scope_types} for API keys issued in this org, combining role and override. Member+ only, details responses only.
org.capabilities.credential_policy_oauthobjectSame, for OAuth grants. Member+ only, details responses only.
org.capabilities.can_view_compliancebooleanEnterprise only. Whether the calling user may open the audit log, audit export, member/sharing reports, credential list and SIEM stream GET — true for admin+ and for a member holding the compliance_auditor flag, AND org_controls. Member+ only, details responses only. See Compliance & Audit below.
org.capabilities.can_manage_legal_holdsbooleanWhether the calling user may place/list/release legal holds. true for the org owner on any plan (so a lapsed-Enterprise org can still list/release existing holds), or for an auditor AND org_controls. Member+ only, details responses only.
org.capabilities.ai_agentbooleanWhether the calling user is currently allowed to use Ripley Agent chat, AI share generation, and the other ai_agent-governed surfaces in this org — role, override and policy combined. Member+ only, details responses only. See AI, Intelligence & MCP Access Policy.
org.capabilities.ai_intelligencebooleanSame, for turning a workspace's/share's Intelligence on, the semantic search leg, and retrieval.
org.capabilities.ai_metadatabooleanSame, for metadata extraction.
org.capabilities.mcp_accessbooleanWhether a request classified as MCP from the calling user may currently reach this org.

Error responses

Error CodeHTTP StatusMessageCause
1680 (Access Denied)401"You have not been granted access to this Org."Insufficient permission

Get Onboarding Checklist

GET /current/org/{org_id}/onboarding/

Auth required. Returns the org's onboarding checklist: whether this org is eligible for Fastio's getting-started checklist, whether an app should show it right now, and — only while it should be shown — the recommended next step and the completion status of each checklist item. Read-only — this call never changes anything.

{org_id} accepts a 19-digit numeric ID or the org's domain name.

Access

RoleAccess
Owner / Admin / MemberFull response
Guest / view-only / non-memberStandard org “not authorized” error

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/onboarding/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "onboarding": {
    "eligible": true,
    "visible": true,
    "next": "invite_teammate",
    "items": [
      {"id": "add_files",       "status": "done",    "completed_at": null},
      {"id": "install_desktop", "status": "todo",    "completed_at": null},
      {"id": "connect_cloud",   "status": "started", "completed_at": null},
      {"id": "connect_agent",   "status": "done",    "completed_at": "2026-10-03 14:02:11 UTC"},
      {"id": "invite_teammate", "status": "todo",    "completed_at": null},
      {"id": "create_portal",   "status": "todo",    "completed_at": null},
      {"id": "ask_ripley",      "status": "todo",    "completed_at": null}
    ]
  }
}

Response fields

FieldTypeDescription
onboarding.eligiblebooleanWhether this is the first organization its owner has ever owned. Joining someone else's org as a member does not count, so a user who was invited to another org and then creates their own first org is eligible on it. false for an owner's second (or later) org, including when the owner already used their free trial on an earlier org.
onboarding.visiblebooleanWhether an app should show the checklist right now: eligible AND the org's subscription is currently in its free trial AND fewer than 14 days have passed since the subscription started. Turns false once the trial converts to paid, the plan ends, or day 14 passes; an org that subscribed directly without a trial is never true.
onboarding.nextstring/nullThe recommended next item's id, in priority order (add_files, invite_teammate, connect_agent, ask_ripley, create_portal, install_desktop, connect_cloud), or null when every item is done, the next item can't currently be determined, or visible is false.
onboarding.itemsarrayPopulated only while visible is true: these seven items, in this fixed order. An empty array while visible is false.
onboarding.items[].idstringOne of add_files, install_desktop, connect_cloud, connect_agent, invite_teammate, create_portal, ask_ripley.
onboarding.items[].statusstringtodo, started (evidence the step has begun, e.g. a cloud import mid-sync or a pending invitation), done, or unknown (could not be determined right now — treat as not done; resolves on a later call).
onboarding.items[].completed_atstring/nullCompletion time, Y-m-d H:i:s UTC, when known; otherwise null — never estimated.

While visible is true, item statuses can take up to a minute to reflect a change.

Each item, when done: add_files — the org has at least one file; install_desktop — a member has signed in to the Fastio Desktop app; connect_cloud — a cloud import (Dropbox/Google Drive/Box/OneDrive) has completed a sync at least once; connect_agent — a member has connected an AI agent to the org; invite_teammate — someone other than the owner has joined the org; create_portal — the org has created a portal; ask_ripley — Ripley has answered a question in one of the org's workspaces.

Error responses

HTTP StatusCause
401/403Caller is not an available member of the org
404The org is closed or no longer exists
503The checklist could not be read right now — retry

Get Public Org Details

GET /current/org/{org_id}/public/details/

No authentication required. Returns limited public info about an org (name, domain, assets). IP-rate-limited.

{org_id} accepts a 19-digit numeric ID or the org's domain name.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/public/details/"

Response (200 OK)

{
  "result": true,
  "org": {
    "id": "1234567890123456789",
    "domain": "acme-corp",
    "name": "Acme Corporation",
    "description": "Leading provider of innovation",
    "logo": "https://assets.fast.io/org/logo.png",
    "accent_color": {"color": "#0066CC", "opacity": 100}
  }
}

Response fields

FieldTypeDescription
org.idstring19-digit numeric organization ID
org.domainstringURL-safe subdomain
org.namestring/nullDisplay name
org.descriptionstring/nullDescription
org.logostring/nullLogo asset URL
org.accent_colorobject/nullBrand color ({color, opacity})
org.login_optionsobjectWhich sign-in routes an org-scoped sign-in page may offer — see login_options in the SSO reference. Omitted when it cannot be determined.

Update Organization

POST /current/org/{org_id}/update/

Auth required. Admin or above. Updates org details. Only provided fields are modified.

Access levels

RoleAccess
OwnerFull access
AdminFull access
MemberDenied

Request parameters (all optional)

NameTypeDescription
domainstringNew URL-safe subdomain (2-63 chars, lowercase alphanumeric + hyphens).
namestringDisplay name (3-100 chars). Cannot be cleared.
descriptionstringDescription. Send "null" or "" to clear.
industrystringIndustry type from predefined list.
accent_colorstring (JSON)Brand accent color as a JSON color object, {"color":"#RRGGBB","opacity":0-100}. Send "null" to clear.
background_colorstring (JSON)Background color as a JSON color object, {"color":"#RRGGBB","opacity":0-100}. Send "null" to clear.
background_modestringBackground display mode.
use_backgroundstringEnable/disable background ("true"/"false").
facebook_urlstring (URL)Facebook URL.
twitter_urlstring (URL)Twitter URL.
instagram_urlstring (URL)Instagram URL.
youtube_urlstring (URL)YouTube URL.
homepage_urlstring (URL)Organization website URL.
perm_member_managestringMember management permission level.
perm_workspace_createstringMinimum role required to create a workspace. Enterprise plan only to tighten (raising the minimum role); lowering it or resubmitting it unchanged works on any plan.
workspace_create_allowliststring (JSON)User IDs that may create a workspace regardless of the threshold. Send the list as a JSON string in this one field, form-encoded or as a query parameter (workspace_create_allowlist=["123","456"]) — a JSON request body is not read. Send [] to clear, which withdraws every exception and is therefore a tightening. Every ID must be a current member. Maximum 500. Enterprise plan only to tighten (removing a current member from the list); adding users or resubmitting it unchanged works on any plan.
sharing_sharesstringWhether members may create shares. Send the string "true" or "false", form-encoded or as a query parameter. Enterprise plan only to tighten (switching it off); resubmitting it unchanged or switching it back on works on any plan.
sharing_file_linksstringWhether members may create single-file share links. Send the string "true" or "false", form-encoded or as a query parameter. Enterprise plan only to tighten (switching it off); resubmitting it unchanged or switching it back on works on any plan.
external_invites_sharesstring (JSON)Collaboration-policy envelope governing Shared Folder and File Share invitations. Send the whole envelope as a JSON string ({"admin":"allowed","member":"denied","overrides":{}}). Send "" or "null" to clear (permissive). Enterprise plan only to tighten. See Collaboration Policies.
external_invites_portalsstring (JSON)Same shape, governing Portal invitations.
external_invites_workspacesstring (JSON)Same shape, governing Workspace invitations.
credential_policystring (JSON)Caps on what API keys and OAuth grants issued in this org may hold — see Credential Policy. Send the whole {api_keys, oauth} object as a JSON string. An omitted family is unchanged; a family set to null clears it. Send "" or "null" to clear the whole key (permissive). Enterprise plan only to tighten.
cloud_syncstring (JSON)Cloud-sync policy envelope — see Cloud Sync Policy. Send the whole envelope as a JSON string ({"admin":{"enabled":true,"mode":"read_write"},"member":{"enabled":true,"mode":"read"},"overrides":{}}). Each value takes exactly enabled (boolean) and mode ("read" or "read_write"). Send "" or "null" to clear (permissive). Enterprise plan only to tighten (switching enabled off, or narrowing read_write to read).
auth_require_2fastring (JSON)Policy envelope requiring a second factor at password/social login — see Require-2FA Policy. Send the whole envelope as a JSON string ({"admin":"required","member":"optional","overrides":{}}). Send "" or "null" to clear (permissive). Refused with two_factor_admin_unenrolled if it tightens the writer's own requirement and they hold no factor. Enterprise plan only to tighten.
access_policystring (JSON)Geo / IP access-restriction envelope — see Access Policy (Geo / IP Restrictions). Send the whole value as a JSON string: {"admin":{"countries":...,"ips":...},"member":{...},"overrides":{"{user_id}":{...}}}. Send "" or "null" to clear (unrestricted). The whole value is replaced on every write — overrides are not merged. Enterprise plan only to tighten.
ai_agentstring (JSON)Policy envelope ({"admin":"allowed"|"denied","member":...,"overrides":{}}) governing Ripley Agent chat (create/send/publish/rename), AI share generation, share auto-title/OG image, events summarize, and dashboard AI. Send "" or "null" to clear (allowed). Enterprise plan only to tighten. See AI, Intelligence & MCP Access Policy.
ai_intelligencestring (JSON)Same envelope shape, governing whether the calling user may turn a workspace's/share's intelligence on, use the meaning-based (semantic) search leg, and have Ripley Agent retrieve from an indexed workspace's index.
ai_metadatastring (JSON)Same envelope shape, governing metadata extraction — single-file, per-folder, compound search — and automatic extraction on ingest.
ai_summariesstring (JSON)Same envelope shape, governing per-file AI summaries at ingest. Background only — this key never produces an interactive refusal, and the editor has no per-user override controls for it. Denying it also stops new files from being indexed, because indexing a file needs its AI summary first; already-indexed content is unaffected.
mcp_accessstring (JSON)Same envelope shape, governing whether a request classified as coming from the Fastio MCP may reach this org at all. Unconfigured means allowed.
ai_workspacesstring (JSON)Allowlist gating background Intelligence/metadata/summary processing and interactive Intelligence/metadata for specific workspaces. A JSON array of workspace id strings, e.g. ["4123456789012345678"]. Send null or "" to allow every workspace (the default); send [] to allow none. Cap 1000 ids. An id naming a closed, deleted, or foreign (not-this-org) workspace is silently pruned on save rather than rejected — the echo returns the pruned list. Narrowing the list (removing an id, or going from null to a list) is a tightening write, except removing an id that was already pruned-on-save (closed, deleted, or foreign) — that never counts as tightening, since it was never really “the org's” for this purpose. A merely suspended or locked workspace still counts as the org's, so removing one of those IS a tightening.
security_alertsstring (JSON)Per-alert opt-out for the five security alert types plus auditor recipients — see Security Alerts below. Send the whole envelope as a JSON string: {"enabled":["login_new_country","geo_policy_block","mass_delete","mass_download","credential_created"],"include_auditors":true}. include_auditors is optional (default true). Send "" or "null" to clear (the default: every alert on, auditors included). enabled: [] turns every alert off. audit_stream_paused is always on and is not a valid name here. Enterprise plan only to turn an alert on, or to turn auditors on; turning alerts off, removing auditors, or clearing is allowed on any plan.
perm_authorized_domainsstringAuthorized email domain for auto-join.
billing_emailstring (email)Billing contact email. Domain must be reachable.
owner_definedstring (JSON)Custom owner-defined properties. Send "null" or "" to clear.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/update/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "name=Acme Corp Updated" \
  -d "description=Updated description" \
  -d "industry=technology"

Response (200 OK)

{
  "result": true
}

If no actual changes are detected, returns success immediately.

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"An invalid org domain was supplied."Invalid domain format
1605 (Invalid Input)406"The supplied org domain name is restricted."Domain is reserved
1605 (Invalid Input)406"The supplied org domain name is already in use."Domain taken by another org
1605 (Invalid Input)406"An invalid configuration was supplied..."Metadata validation failed
1605 (Invalid Input)406"Invalid JSON provided for {key}."Malformed JSON
1605 (Invalid Input)406"The email domain is invalid or cannot receive email."Bad billing email domain
1605 (Invalid Input)406"The workspace-create allowlist must be a list of user ids."workspace_create_allowlist is not a JSON list of user IDs
1605 (Invalid Input)406"Every user on the workspace-create allowlist must be a member of this org."An allowlist entry is not a current member
1605 (Invalid Input)406"The workspace-create allowlist may name at most 500 users."Allowlist over the cap
1605 (Invalid Input)406"A policy must be submitted as a JSON-encoded object."A collaboration-policy field was not a JSON string
1605 (Invalid Input)406"A policy must be an object with "admin" and "member" set to "allowed" or "denied", and an optional "overrides" object of user id to the same values."Malformed collaboration-policy envelope
1605 (Invalid Input)406"A policy may name at most 100 per-user exceptions."Overrides over the cap
1605 (Invalid Input)406"Every user named in a policy exception must be a member of this org."An override names a non-member
1605 (Invalid Input)406"access_policy: " followed by the specific problem — "A policy must be submitted as a JSON-encoded object.", or "A policy must be an object with \"admin\" and \"member\" set to an object with \"countries\" (…) and \"ips\" (…), and an optional \"overrides\" object of user id to the same values."access_policy failed validation: a countries.codes list outside 1-250 entries or containing something other than a 2-letter code (XX unknown and T1 Tor are accepted, as is any other 2-letter code including XK), an ips list outside 1-100 entries, an unparseable IPv4/IPv6 address or CIDR, or a /0 prefix (refused outright — clear the rule instead of writing it as "unrestricted"). params[].name = access_policy.
1605 (Invalid Input)406"ai_workspaces: The AI workspace list must be null, or a JSON list of workspace ids." or "ai_workspaces: The AI workspace list may name at most 1000 workspaces."ai_workspaces was not a JSON array, contained an entry that is not a valid workspace id, or named more than 1000 workspaces. params[].name = ai_workspaces. (An id for a workspace that is merely closed, deleted, or in another org is pruned automatically, not rejected — see the field description above.)
1605 (Invalid Input)406"security_alerts: " followed by the specific problem (e.g. "Unknown security alert name. Allowed: …")security_alerts failed validation: an unknown alert name in enabled, a non-list enabled, a non-boolean include_auditors, or an unknown field. params[].name = security_alerts.
1700 (Forbidden)403"This configuration requires an Enterprise plan."A security-control write that tightens a setting — including access_policy, ai_agent, ai_intelligence, ai_metadata, ai_summaries, mcp_access, narrowing ai_workspaces, or security_alerts turning an alert on or turning auditors on — was sent by an org without the entitlement. Resubmits and relaxations are not refused. params.reason = plan_required.
1700 (Forbidden)403"This change would block your own access from your current location or network. Add your own IP address or a per-user exception in the same change."An access_policy write would take the caller's own current IP/country from pass to block. params.reason = access_policy_self_lockout, plus params.ip / params.country. There is no override flag to bypass this — add your own IP, or an override for yourself, in the same write. See Access Policy (Geo / IP Restrictions).
1693 (Temporarily Unavailable)503"This policy could not be checked against your own access. Please try again."An access_policy write's self-lockout check could not be resolved (the caller's own role could not be read). params.reason = access_policy_unavailable. Retryable — nothing was saved.
1693 (Temporarily Unavailable)503"The workspaces named in this policy could not be verified. Please try again."An ai_workspaces write named a workspace whose status could not be read, so it could be neither kept nor pruned. Retryable — nothing was saved.
1700 (Forbidden)403"This change would require two-factor authentication of your own account, which has none enrolled. Enrol a second factor first."An auth_require_2fa write tightens the WRITER's own effective requirement from optional to required and the writer holds no factor. params.reason = two_factor_admin_unenrolled. Tightening the member baseline, or an override on another user, is not refused.
1693 (Temporarily Unavailable)503"This policy could not be checked against your own account. Please try again."An auth_require_2fa write could not be evaluated against the writer's own account, so neither the self-lockout refusal above nor a pass could be established. Retryable — nothing was saved. Re-send the same request.
1693 (Temporarily Unavailable)503"This policy change could not be scheduled. Please try again."An auth_require_2fa write TIGHTENED the policy, but the session revocation that tightening requires could not be scheduled. Retryable, and the write was deliberately abandoned — the policy was NOT stored. Never treat this as “probably applied”; re-send the same request.
1693 (Temporarily Unavailable)503"This AI policy change could not be recorded. Please try again."A write that pauses background AI processing — denying ai_intelligence, ai_metadata, or ai_summaries for both the admin and member baselines, or narrowing ai_workspaces — could not be recorded. (ai_agent and mcp_access have no background processing, so a write to them never returns this.) Retryable, and the write was deliberately abandoned — nothing was saved. See AI, Intelligence & MCP Access Policy above.
1663 (Update Failed)500"There was an internal error processing your update request."Internal error

Close Organization

POST /current/org/{org_id}/close/

Auth required. Owner only. Soft-deletes the organization. Active subscriptions are automatically cancelled; if the subscription cannot be cancelled, the organization is not closed and the call returns 503 — retry.

Request parameters

NameTypeRequiredDescription
confirmstringYesMust match the org domain name or org numeric ID as confirmation.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/close/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "confirm=acme-corp"

Response (202 Accepted)

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
120445406"The confirm field is required. Pass the org's domain or numeric id as confirm."confirm was not provided
10549406"The confirm field provided does not match the org's domain or id."Confirmation does not match domain or ID
113130409"This organization cannot be closed while a legal hold is active. Release all legal holds first."An active legal hold — see Legal Holds below
163648503"The organization cannot be closed right now. Please try again shortly."Legal-hold state could not be read; retry
108611503"The organization cannot be closed right now. Please try again shortly."Billing was busy with another change while cancelling the subscription; nothing was closed — retry
150073503"The subscription could not be cancelled, so the organization was not closed. Please try again shortly."The subscription could not be cancelled; nothing was closed — retry
1663 (Update Failed)500"There was an internal error processing your request."Failed to close org

Storage deletion is deferred to the deletion system after a retention period.


Organization Assets

List Available Asset Types

GET /current/org/assets/

Auth required. Returns available org asset metadata types (e.g., logo, background images).

curl example

curl -X GET "https://api.fast.io/current/org/assets/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "names": ["logo", "background"],
  "metadata_scheme": {
    "logo": {
      "width": {"name": "Image Width", "description": "Image width", "required": true, "type": "int", "min": 1},
      "height": {"name": "Image Height", "description": "Image height", "required": true, "type": "int", "min": 1},
      "mime": {"name": "Mimetype", "description": "Image mimetype", "required": true, "type": "string"},
      "megapixels": {"name": "Image Megapixels", "description": "Image megapixels", "required": true, "type": "int", "min": 0, "max": 48},
      "transform": {"name": "transform", "description": "Transform image", "required": false, "type": "image_transformer"}
    }
  },
  "file_types": {"logo": "image", "background": "image"}
}

names lists the asset names the org accepts; file_types maps each to its file kind; metadata_scheme maps each to the metadata properties validated on upload (shown for logo only — background carries the same keys, with width/height capped at 4096 and megapixels at 33).


List Org Assets

GET /current/org/{org_id}/assets/

Auth required. Any member with at least View permission. Returns assets currently set on the org.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/assets/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "assets": {
    "logo": {
      "metadata": {"width": 512, "height": 512, "megapixels": 0, "mime": "image/png"}
    }
  }
}

Keyed by asset name; each entry carries the stored metadata (image width, height, megapixels, mime). An org with no assets returns "assets": []. Fetch the bytes with Read Org Asset (Raw) below.


Upload Org Asset

POST /current/org/{org_id}/assets/{asset_name}/

Auth required. Admin or above. Upload as multipart/form-data.

Request parameters

NameTypeRequiredDescription
filefile (multipart)YesThe asset file to upload.
metadatastring (JSON object)NoAdditional metadata for the asset, sent as a JSON object string (e.g. {}).

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/assets/logo/" \
  -H "Authorization: Bearer {jwt_token}" \
  -F "file=@logo.png"

Error responses

Error CodeHTTP StatusMessageCause
1691 (File Missing)412"Asset upload missing"No file in the request
100289406"metadata must be a JSON object encoded as a string."metadata is not a JSON object

Delete Org Asset

DELETE /current/org/{org_id}/assets/{asset_name}/

Auth required. Admin or above.

curl example

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/assets/logo/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true
}

Read Org Asset (Raw)

GET /current/org/{org_id}/assets/{asset_name}/read/

No authentication required. Returns the raw binary content of an org asset with appropriate Content-Type header. Useful for displaying logos and images directly.

HEAD requests return headers only.


Organization Members

Add or Invite a Member

POST /current/org/{org_id}/members/{email_or_user_id}/

Auth required. Permission governed by org's perm_member_manage setting. Parameters are form fields (application/x-www-form-urlencoded or multipart/form-data); a JSON request body is refused with a 406 error. You cannot add or invite someone at a role above your own — the ceiling applies to invitations as well as direct adds.

The target is specified as a path parameter:

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 parameters (adding existing user by ID)

NameTypeRequiredDescription
permissionsstringYesPermission level: "member", "admin". Cannot add as "owner". A value other than admin, member, guest or view (including any) is refused with a 406 error rather than treated as "member".
expiresstring (datetime)NoMembership expiration date.
notify_optionsstringNoNotification preference.
notificationstringNoSend force to force the notification email to the added user.

Request parameters (inviting by email)

NameTypeRequiredDescription
permissionsstringYesPermission level for the invitation: "member", "admin". A value other than admin, member, guest or view (including any) is refused with a 406 error rather than treated as "member".
messagestringNoCustom invitation message.
expiresstring (datetime)NoExpiration of the membership granted when the invitation is accepted.
invitation_expiresstring (datetime)NoDeadline for accepting the invitation (must be in the future).

curl example (add existing user)

curl -X POST "https://api.fast.io/current/org/1234567890123456789/members/9876543210987654321/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "permissions=member"

curl example (invite by email)

curl -X POST "https://api.fast.io/current/org/1234567890123456789/members/jane@example.com/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "permissions=member" \
  -d "message=Welcome to the team!"

Response (200 OK) — direct add

{
  "result": true
}

Response (200 OK) — invitation created

{
  "result": true,
  "invitation": {
    "id": "a53no-nj43t-cwoju-ldoi6-nxql4-hm5q",
    "invitee_email": "jane@example.com",
    "entity_type": "org",
    "state": "pending",
    "created": "2024-01-15 10:30:00 UTC"
  }
}

Abbreviated: the invitation object carries every field of a List Org Invitations entry, plus an org object.

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"Invalid permission specified."permissions missing
1605 (Invalid Input)406"Invalid permissions value. Valid values are: admin, member, guest, view."permissions is not one of those role names
1605 (Invalid Input)406"This endpoint does not accept a JSON request body..."Request sent with a JSON body
1692 (Cannot Add As Owner)406"Adding a member as an owner is not allowed"Tried to add as owner (use transfer_ownership)
127022406"You cannot add, update, or delete a membership with a higher permission than your own."The requested role is above your own — for an invitation as well as a direct add
1656 (Limit Exceeded)413Limit messageMember limit exceeded

Remove a Member

DELETE /current/org/{org_id}/members/{user_id}/

Auth required. Permission governed by org's perm_member_manage setting.

The target user ID (19-digit numeric) is a path parameter.

curl example

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/members/9876543210987654321/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true
}

List Org Members

GET /current/org/{org_id}/members/list/

Auth required. Any org member. Paginated.

Query parameters

NameTypeDefaultDescription
limitinteger1001-500, number of items to return
offsetinteger0Number of items to skip

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/members/list/?limit=50&offset=0" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "users": [
    {
      "id": "1234567890123456789",
      "account_type": "human",
      "email_address": "owner@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "permissions": "owner",
      "auth": {
        "password": true,
        "social": [
          {"provider": "google", "last_login": "2026-09-15 10:22:31 UTC"}
        ],
        "sso": [],
        "two_factor": {"enabled": true, "method": "totp"}
      },
      "compliance_auditor": false
    },
    {
      "id": "1234567890123456780",
      "account_type": "agent",
      "email_address": "bot@example.com",
      "first_name": "Service",
      "last_name": "Bot",
      "permissions": "admin",
      "auth": {
        "password": false,
        "social": [],
        "sso": [],
        "two_factor": {"enabled": false, "method": null}
      },
      "compliance_auditor": false
    }
  ],
  "pagination": {
    "total": 2,
    "limit": 50,
    "offset": 0,
    "has_more": false
  }
}

Response fields

FieldTypeDescription
usersarrayArray of member objects
users[].idstring19-digit numeric user ID
users[].account_typestring"human" or "agent"
users[].email_addressstringUser's email
users[].first_namestringFirst name
users[].last_namestringLast name
users[].permissionsstringRole: "owner", "admin", "member"
users[].authobjectSign-in methods for this member. Present only when the caller is an owner, admin, or holder of the org's compliance_auditor flag — owners and admins on every plan; a compliance_auditor holder only while the org has the Enterprise entitlement — absent, not null, for any other viewer, and also absent if a supporting table could not be read. Included at default output and ?output=standard; omitted at ?output=terse.
users[].auth.passwordbooleanWhether the member has a usable password.
users[].auth.socialarrayProviders (google, microsoft) the member has signed in with — each entry is {provider, last_login}, where last_login is the most recent sign-in with, or connection of, that provider (a freshly connected provider shows its connection time).
users[].auth.ssoarrayThis org's active SSO identities for the member — each entry is {org_id, protocol, last_login}; protocol is saml/oidc and is null until that identity's first SSO login. Lists only identities in the org being listed, not the member's other orgs.
users[].auth.two_factorobject{enabled, method} — method is totp or phone, null when 2FA is disabled.
pagination.totalintegerTotal number of members
pagination.limitintegerRequested page size
pagination.offsetintegerCurrent offset
pagination.has_morebooleanWhether more results exist

Leave Organization (Self)

DELETE /current/org/{org_id}/member/

Auth required. Removes the authenticated user from the org. Owners cannot leave — they must transfer ownership or close the org first.

curl example

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/member/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"You cannot leave an org you are the owner of..."User is the org owner
1605 (Invalid Input)406"You cannot leave an org you are not a member of."User is not a member

Get Member Details

GET /current/org/{org_id}/member/{user_id}/details/

Auth required. Any org member.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/details/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "user": {
    "id": "9876543210987654321",
    "account_type": "human",
    "email_address": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "permissions": "admin",
    "notify": "Notify me in app",
    "member_added_at": "2026-08-29 15:48:29 UTC"
  }
}

Response fields

FieldTypeDescription
user.idstring19-digit numeric user ID
user.account_typestring"human" or "agent"
user.email_addressstringUser's email
user.first_namestringFirst name
user.last_namestringLast name
user.permissionsstringRole: "owner", "admin", "member"
user.inviteobjectPending-invitation snapshot (id, created, expires); absent when unset, which is normal for an active member
user.notifystringNotification preference — present only when you read your own membership
user.expiresstringMembership expiration (YYYY-MM-DD HH:MM:SS UTC); absent for a permanent membership
user.member_added_atstringWhen this membership was created (YYYY-MM-DD HH:MM:SS UTC). Absent — not null — when you are not allowed to see it: emitted only to the member themselves or to an admin-or-above of this org, so a peer member gets no key at all. Read it with a presence check on the key
user.compliance_auditorbooleanWhether this member holds the compliance-auditor flag (read-only compliance surfaces plus legal-hold management, granted by the owner — see Compliance & Audit below). Present on every plan — inert (grants nothing) while the org lacks Enterprise. Absent, not false, unless the viewer is the member themselves, an org admin+, or an auditor while the org has Enterprise — a peer member gets no key at all. Also present on org members list (GET /current/org/{org_id}/members/list/) rows under the same visibility rule.

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"The membership you specified does not exist."User is not a member

Update Member Permissions

POST /current/org/{org_id}/member/{user_id}/update/

Auth required. Permission governed by org's perm_member_manage setting. Parameters are form fields; a JSON request body is refused with a 406 error.

Request parameters (all optional)

NameTypeDescription
permissionsstringNew permission level ("member", "admin"). Omitted leaves the role unchanged; "owner" is ignored (use Transfer Org Ownership). A value other than admin, member, guest or view (including any) is refused with a 406 error.
expiresstring (datetime)Membership expiration date
notify_optionsstringNotification preference; omitted leaves it unchanged

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/update/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "permissions=admin"

Response (200 OK)

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"The membership you specified does not exist."User is not a member
1605 (Invalid Input)406"Invalid permissions value. Valid values are: admin, member, guest, view."permissions is not one of those role names
1605 (Invalid Input)406"This endpoint does not accept a JSON request body..."Request sent with a JSON body
127022406"You cannot add, update, or delete a membership with a higher permission than your own."The resulting role is above your own

Transfer Org Ownership

POST /current/org/{org_id}/member/{user_id}/transfer_ownership/

POST only — GET, HEAD, and every other method return 405. Auth required. Owner only. Transfers ownership of the org to the specified member, who must be an enabled, non-phantom org member with role member or above (not guest/viewer). The current owner is demoted to admin.

Fixed behaviour:

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/transfer_ownership/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "ownership": {
    "profile_id": "1234567890123456789", "profile_type": "org",
    "previous_owner": "1111111111111111111", "new_owner": "9876543210987654321",
    "transferred_at": "2026-09-23 16:37:29 UTC"
  }
}

Error responses (error.params.reason)

ReasonHTTPMeaning
successor_is_self406"You cannot transfer ownership to yourself." — target is the current user
successor_not_member406"The membership you specified does not exist." — user is not an org member, or their membership has been removed or has expired
successor_unavailable406"The new owner's account is not active, so ownership cannot be transferred to it." — target is closed, suspended, locked, or a phantom member
successor_role_too_low406"The new owner must be a member of the organization, not a guest or viewer." — target's role is below member (guest/view)
successor_org_limit406"The new owner already owns the maximum number of free organizations." — org is free/unpaid and the receiver is already at the free-org limit
transfer_in_progress409"Another ownership transfer of this org is in progress. Please try again shortly." — another transfer of this org is running; retry shortly
—401"Appropriate access is not granted to this Org." / "You are no longer the owner of this org." — caller is not the owner (the second text: ownership changed while the call was waiting)
scope_admin_required403The credential is not admin-capable on this org (an API key/OAuth token without rwa)
—500"The ownership transfer did not finish. Transfer to the same member again to complete it." — repeat the call with the same target to complete it
—503"Ownership transfer is temporarily unavailable. Please try again shortly." — retry

Event: ownership_transferred (audit log; profile_type, from_user, to_user). The two existing membership_updated events (promotion and demotion) still fire.


Bulk Access Transfer (Org)

Move a departing or off-boarding member's entire access footprint in one org to another member — every workspace and share membership, plus the ownerships on them — in one guided flow: preview, execute, poll status.

Scope: org O only — A's org row, A's rows on O's workspaces, and A's rows on shares inside O's workspaces. Personal shares and A's memberships in other orgs are untouched.

Rules:

Preview (dry run)

POST /current/org/{org_id}/member/{user_id}/transfer_access/preview/

{user_id} is A. Body: to_user_id (required, B's user id). No writes.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/transfer_access/preview/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "to_user_id=1111111111111111111"

Response (200 OK)

{
  "result": true,
  "preview": {
    "from_user": "9876543210987654321", "to_user": "1111111111111111111",
    "plan_hash": "sha256:9f2c…",
    "complete": true,
    "org": {"from_role": "member", "to_role_before": "guest", "to_role_after": "member", "action": "raise"},
    "counts": {"workspaces": 12, "shares": 40, "ownerships": 3, "raise": 30, "add": 20, "skipped_b_higher": 2, "invites_sent_by_from": 4},
    "items": [
      {"type": "workspace", "id": "4123456789012345678", "name": "Legal", "from_role": "owner",
       "to_role_before": null, "to_role_after": "owner", "action": "transfer_ownership"}
    ],
    "items_truncated": false,
    "blocking": [],
    "warnings": ["billable_users_may_increase"],
    "offboard_allowed": true, "offboard_blocked_reason": null
  }
}

Response fields

FieldTypeDescription
preview.plan_hashstringSend this back unchanged with execute
preview.completebooleantrue only when every relationship was provably enumerated. false means counts are lower bounds, and execute is refused with plan_incomplete
preview.orgobjectThe org-level role change; action is add|raise|transfer_ownership|skipped_b_higher|none
preview.countsobjectCovers workspace and share items only — the org row is the separate org block above
preview.items[].actionstringSame vocabulary as preview.org.action
preview.items / items_truncatedarray/booleanCapped at 500 items; counts stay exact even when items are truncated
preview.blockingarray of stringRefusal reasons that would stop execute; a non-empty list still returns 200 here — nothing_to_transfer can appear
preview.warningsarray of stringMachine codes for client-owned copy: billable_users_may_increase, pending_invites_unchanged
preview.offboard_blocked_reasonstring/nullnull when the offboard is not blocked. restricted — blocked by a retention or compliance restriction on the member (the specific restriction is not disclosed). An org owner or entitled auditor caller may see a more specific value, legal_hold, instead

Errors: 403 insufficient_permission (caller is not admin+, or an admin targeting an admin — this refuses the preview outright); 406 successor_not_org_member (malformed to_user_id); 406 from_user_not_member (malformed {user_id} path segment); 406 field validation. Every other refusal (from_user_not_member, from_user_is_org_owner, successor_is_source, successor_not_org_member, successor_unavailable, nothing_to_transfer) appears in blocking with HTTP 200 instead of failing the call.

Execute

POST /current/org/{org_id}/member/{user_id}/transfer_access/

Body: to_user_id (required); plan_hash (required, from the preview); offboard (bool, default false).

The server recomputes the plan and compares hashes — a mismatch means something changed since the preview.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/transfer_access/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "to_user_id=1111111111111111111" -d "plan_hash=sha256:9f2c…" -d "offboard=true"

Response: HTTP 202

{"result": true, "transfer": {"id": "mt7q…", "status": "queued", "created_at": "2026-09-23 16:40:00 UTC"}}

Errors (error.params.reason), checked in this order: 403 insufficient_permission; the first applicable blocking reason (406); 409 plan_incomplete (preview could not enumerate everything); 409 plan_changed (re-preview); 409 transfer_in_progress (params.transfer_id names the active transfer — show it); 503 (store/queue unavailable, retry); 429 throttled.

Events: org_member_transfer_started (plan counts). Per item, the existing added-member events / membership_updated, plus ownership_transferred, each carrying transfer_id. At the end, org_member_transfer_completed (report counts, complete, offboard_status).

Status + report

GET /current/org/{org_id}/transfers/{transfer_id}/

Who: org admin+. Query: limit (1-500, default 100), offset over items. Poll every 2-5 s while status is queued or running.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/transfers/mt7q…/?limit=100&offset=0" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "transfer": {
    "id": "mt7q…", "status": "running", "from_user": "9876543210987654321", "to_user": "1111111111111111111",
    "calling_user": "1234567890123456780",
    "offboard": true, "offboard_status": "not_requested", "offboard_blocked_reason": null, "reason": null,
    "counts": {"total": 55, "done": 30, "skipped": 2, "failed": 0},
    "items": [{"type": "share", "id": "5123…", "action": "add", "outcome": "done", "reason": null}],
    "created_at": "… UTC", "updated_at": "… UTC", "completed_at": null
  },
  "pagination": {"total": 55, "limit": 100, "offset": 0, "has_more": false}
}

Response fields

FieldTypeDescription
transfer.statusstringqueued|running|completed|completed_with_errors|failed
transfer.reasonstring/nullnull or report_lost — the progress record was lost; the audit log remains the record
transfer.offboard_statusstringnot_requested|dispatched|blocked|failed. dispatched means A was removed from the org and workspace/share clean-up started in the background. blocked pairs with offboard_blocked_reason
transfer.itemsarrayIncludes the org row (type: "org", so counts.total = items + 1). outcome: done|skipped_b_higher|skipped_gone|failed. Item reason on a failed item: commit_failed|read_failed
transfer.counts.failedintegerAlso counts memberships whose own org could not be confirmed as this org (a transient read failure) — those never appear in items, since it isn't known that they belong here

Kept for 30 days; after that, 404 transfer_not_found — the audit log remains the record.


Join Organization

POST /current/org/{org_id}/members/join/

Auth required. Join an org via invite or domain-based auto-join.

Join methods

  1. Via invitation: Append the invitation key to the URL path: .../join/{invitation_key}/ optionally followed by accept or decline. Default is accept.
  2. Via authorized domain: The org must have perm_authorized_domains set, the user's email address must be verified (agent accounts included), and the user's email domain must match. A new user is added as a Member; a current (unexpired) member keeps their role; an expired membership is restored as Member. Only notify_options is read from input; permissions is ignored.

curl example (invitation)

curl -X POST "https://api.fast.io/current/org/1234567890123456789/members/join/abc123def456/accept/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
1680 (Access Denied)401"This org does not allow you to join automatically..."Domain auto-join not enabled
10587401"Verify your email address to join this org automatically, or request an invitation."Domain auto-join attempted by an account whose email address is not verified
1680 (Access Denied)401"You are not allowed to join this org automatically..."User's email domain does not match
1656 (Limit Exceeded)413Limit messageMember limit exceeded
146157406"This invitation is not for this resource."The invitation key is not an invitation to this org ({org_id}) — for example a workspace or share invitation, or another org's. Nothing is accepted or declined

List Org Invitations

GET /current/org/{org_id}/members/invitations/list/

Auth required. Any org member. An optional state filter can be appended: .../list/pending/.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/members/invitations/list/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "invitations": [
    {
      "id": "a53no-nj43t-cwoju-ldoi6-nxql4-hm5q",
      "inviter": "John Doe",
      "inviter_actor": {
        "user_id": "1234567890123456789",
        "kind": "human",
        "agent_name": null,
        "name_source": null,
        "credential_type": "session",
        "verified": false
      },
      "invitee_email": "jane@example.com",
      "invitee_uid": "5566778899001122334",
      "accepted_uid": null,
      "entity_type": "org",
      "state": "pending",
      "consumed": false,
      "created": "2024-01-15 10:30:00 UTC",
      "updated": "2024-01-15 10:30:00 UTC",
      "expires": "2024-01-18 10:30:00 UTC"
    }
  ]
}

Response fields

FieldTypeDescription
invitationsarrayArray of invitation objects
invitations[].idstringInvitation identifier
invitations[].inviterstringName of the user who sent the invitation
invitations[].inviter_actorobjectWho sent the invitation and whether an agent acted for them: user_id, kind (human, agent, api_key, app, system, unknown), agent_name, name_source, credential_type, verified. Any agent_name other than Fastio’s own verified agent is self-declared. Full reference: Actor Attribution in the Storage reference
invitations[].invitee_emailstringEmail address of the invitee
invitations[].invitee_uidstring/nullUser ID (19-digit string) of the invitee's pending-member placeholder; null when there is none
invitations[].accepted_uidstring/null19-digit user ID of the account that accepted, as a string; null until accepted
invitations[].entity_typestringAlways "org" for org invitations
invitations[].statestringInvitation state: "pending", "accepted", "declined"
invitations[].consumedbooleantrue once the invitation has been accepted
invitations[].createdstringCreation timestamp
invitations[].updatedstringLast update timestamp
invitations[].expiresstring/nullDeadline for accepting the invitation

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"An invalid invitation state was supplied."Invalid state filter

Update an Invitation

POST /current/org/{org_id}/members/invitation/{invitation_id}/

Auth required. Permission governed by org's perm_member_manage setting.

{invitation_id} can be the invitation ID or the invitee email address. The invitation must belong to this org; otherwise the request fails with 406 exactly as an unknown invitation does ("Invalid invitation id or Invitation not found." for an ID). Parameters are form fields; a JSON request body is refused with a 406 error.

Request parameters (all optional)

NameTypeDescription
statestringNew invitation state: "pending", "accepted", "declined"
permissionsstringThe role granted when the invitation is accepted. Cannot be "owner" or above your own role; either refusal returns an error and leaves the invitation unchanged
notify_optionsstringNotification preference applied on acceptance
expiresstring (datetime)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

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/members/invitation/a53no-nj43t-cwoju-ldoi6-nxql4-hm5q/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "permissions=admin"

Response (200 OK)

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"An invalid invitation ID or email was supplied."Invalid identifier
1605 (Invalid Input)406"Invalid invitation id or Invitation not found."No invitation with that ID, or it belongs to a different org
1605 (Invalid Input)406"Invitation is not for an Org"Wrong entity type
1605 (Invalid Input)406"An invalid state was supplied."Invalid state value
1605 (Invalid Input)406"Invalid permissions value. Valid values are: admin, member, guest, view."permissions is not one of those role names
1605 (Invalid Input)406"This endpoint does not accept a JSON request body..."Request sent with a JSON body
1692 (Cannot Add As Owner)406"Adding a member as an owner is not allowed"permissions=owner
127022406"You cannot add, update, or delete a membership with a higher permission than your own."The new role is above your own
1679 (Update Failed)500"Failed to update invitation."Internal error

Delete an Invitation

DELETE /current/org/{org_id}/members/invitation/{invitation_id}/

Auth required. Permission governed by org's perm_member_manage setting.

{invitation_id} can be the invitation ID or the invitee email address. The invitation must belong to this org; otherwise the request fails with 406 exactly as an unknown invitation does ("Invalid invitation id or Invitation not found." for an ID).

curl example

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/members/invitation/a53no-nj43t-cwoju-ldoi6-nxql4-hm5q/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
1666 (Delete Failed)500"Failed to delete invitation."Deletion failed

Compliance & Audit

Enterprise plan. These endpoints give an org owner or admin, or a member holding the compliance_auditor flag (“auditor”), read access to the org’s credential inventory, per-user and sharing-exposure reports, and a streaming audit-log export — plus the two write actions (credential revoke, force sign-out) that stay admin-only. See org.capabilities.can_view_compliance / can_manage_legal_holds above to decide whether to show this UI without guessing from role alone.

The auditor flag adds no write power. It is a boolean on a normal org membership (member.compliance_auditor, granted only by the owner — see Grant/Revoke the Auditor Flag below). An auditor keeps their existing role and workspaces; the flag only opens the read-only surfaces on this page plus legal-hold management. It is inert while the org lacks the Enterprise plan.

Credential rule for these reads. A signed-in browser session always passes. An API key needs an admin-scope (rwa) grant on the org.

Common refusals across this whole section — branch on error.params.reason, never on error.code or on HTTP status alone:

ReasonHTTPMeaning
compliance_access_required403Caller is neither admin+ nor an auditor
scope_admin_required403An API-key credential without an admin-capable (rwa) grant on the org (a browser session always passes)
plan_required403Org is not on the Enterprise plan
member_not_found404{user_id} is not a live org member

Credential Reach Classification

Every API key and OAuth grant is classified relative to the org being viewed:

ReachMeaning
org_onlyEvery scope on the credential targets this org's entities.
user_wideLegacy/NULL scopes claim, user:*:*, or any wildcard scope (org:*, workspace:*, share:*, fileshare:*, sign_envelope:*).
mixedConcrete grants on this org plus other orgs or personal entities.
nullReach could not be resolved. Listed, but never revocable.
(not listed)no_reach credentials (nothing on this org) are omitted entirely; a revoke attempt against one returns 404 credential_not_found.

Revoke eligibility (who may act, and on what):


List Org Credentials

GET /current/org/{org_id}/credentials/

List every API key and OAuth grant that reaches this org, across all members. Auth: admin+ or auditor. Enterprise.

Query parameters

ParameterTypeRequiredDefaultDescription
user_idstringNo—19-digit numeric ID. Restrict to one member. A non-member id returns an empty list.
typestringNoallapi_key | oauth | all
reachstringNo—org_only | user_wide | mixed
limitintegerNo501–50. Counts members per page, not credentials — each member contributes up to 100 credentials (see credentials_capped).
cursorstringNo—Opaque keyset cursor from pagination.next_cursor.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/credentials/?type=oauth&reach=mixed" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "credentials": [
    {
      "type": "api_key", "id": "k8x2mq4rt7", "user_id": "9876543210987654321",
      "label": "CI deploy", "agent_name": null, "client_id": null, "device_name": null,
      "token_chars": "x9Qz", "scopes": ["workspace:4123456789012345678:rw"], "other_scopes": 0,
      "access_mode": "rw", "admin": false, "legacy": false, "mcp": false,
      "reach": "org_only", "created": "2026-09-01 10:00:00 UTC", "expires": null,
      "last_used": "2026-09-23 07:10:00 UTC", "last_ip": "203.0.113.7", "last_country": "US",
      "revocable": true, "revocable_reason": null
    },
    {
      "type": "oauth", "id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "user_id": "9876543210987654321",
      "label": null, "agent_name": "Claude", "client_id": "cimd:https://claude.ai/…",
      "device_name": "MacBook", "token_chars": null, "scopes": ["workspace:4123456789012345679:rw"], "other_scopes": 2,
      "access_mode": "rw", "admin": false, "legacy": false, "mcp": true, "reach": "mixed",
      "created": "2026-09-10 12:00:00 UTC", "expires": "2026-10-10 12:00:00 UTC",
      "last_used": "2026-09-22 18:00:00 UTC", "last_ip": "198.51.100.4", "last_country": "DE",
      "revocable": false, "revocable_reason": "credential_spans_other_orgs"
    }
  ],
  "pagination": {"has_more": true, "next_cursor": "…", "page_size": 50, "credentials_capped": false}
}

Response fields

FieldTypeDescription
credentials[].typestringapi_key | oauth
credentials[].idstringAlphanumeric id (API-key id or 32-hex OAuth session id)
credentials[].user_idstringThe credential owner
credentials[].scopesarray or nullOnly wildcards and this org's own scopes, verbatim; null for a legacy full-access credential. A concrete scope owned by another org, no org, or an unresolvable owner is withheld — see other_scopes
credentials[].other_scopesintegerCount of concrete scopes withheld from scopes because they belong to another org (or could not be resolved). Present on every row; 0 when nothing was withheld. reach still accounts for the withheld scopes
credentials[].reachstring or nullSee Credential Reach Classification above
credentials[].last_used / last_ip / last_countrystring/nullWritten periodically, not on every request
credentials[].mcpbooleantrue for an OAuth grant whose audience is the MCP server
credentials[].revocablebooleanComputed for the caller — an auditor or a non-eligible admin sees false with a reason
credentials[].revocable_reasonstring or nullnull | credential_spans_other_orgs | cannot_act_on_member
pagination.credentials_cappedbooleantrue when at least one member on this page had more than 100 credentials

Token secrets and hashes are never returned. Expired API keys and revoked/expired OAuth sessions are not listed.

Error responses

ReasonHTTP
compliance_access_required / scope_admin_required / plan_required403
validation406

Revoke a Credential

DELETE /current/org/{org_id}/member/{user_id}/credentials/{type}/{credential_id}/

Revoke one credential belonging to {user_id}. Auth: an eligible actor, admin-scope credential required. Enterprise.

{user_id} = the credential's owner (a live org member); {type} = api_key | oauth. No body.

curl example

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/credentials/api_key/k8x2mq4rt7/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{"result": true, "revoked": {"type": "api_key", "id": "k8x2mq4rt7", "user_id": "9876543210987654321"}}

Error responses

ReasonHTTPMeaning
credential_not_found404Gone, no_reach, or does not belong to {user_id}
member_not_found404{user_id} is not a live org member
credential_spans_other_orgs409user_wide/mixed credential and the owner is not on a verified SSO domain
cannot_act_on_member403Target is the owner, a peer, or above the caller's rank
compliance_access_required / scope_admin_required / plan_required403—

Events: org_credential_revoked (audit log). The existing api_key_deleted also fires on the member's own trail, with the calling user as the admin.


Force Sign-Out a Member

POST /current/org/{org_id}/member/{user_id}/sign-out/

Invalidate every session everywhere for this member, and revoke whatever of their credentials the revoke rule allows. Auth: an eligible actor, admin-scope credential required. Enterprise. No body.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/sign-out/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true, "sessions_invalidated": true,
  "revoked": [{"type": "api_key", "id": "k8x2mq4rt7"}, {"type": "oauth", "id": "a1b2…"}],
  "skipped": [{"type": "oauth", "id": "c3d4…", "reason": "credential_spans_other_orgs"}]
}

Error responses

Reason / CauseHTTP
member_not_found404
cannot_act_on_member403
Throttled — honour Retry-After429
compliance_access_required / scope_admin_required / plan_required403

A 500 does not by itself tell you whether sessions were invalidated: it can occur before invalidation (retry) or after invalidation when one or more credential revokes then failed. Retrying is always safe (idempotent): the session bump repeats harmlessly and already-revoked credentials are unaffected.

Event: org_member_signed_out.

Confirmation copy must say: this signs the person out of every Fastio org and personal use, not only this one, and OAuth apps may keep working for up to 1 hour.


Get Member Compliance Report

GET /current/org/{org_id}/member/{user_id}/report/

A best-effort compliance snapshot of one member: identity, org/workspace membership, credential and login counts, last activity. Auth: admin+ or auditor. Enterprise.

Target must be a live org member (404 member_not_found). Each section is independently best-effort — see partial/truncated.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/report/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "report": {
    "user": {
      "id": "9876543210987654321", "email_address": "a@example.com", "first_name": "A", "last_name": "B",
      "created": "2025-01-15 10:30:00 UTC", "two_factor": true, "password_set": true,
      "sso": {"enforced": true, "exempt": false, "org_domain": "example.com"},
      "managed_account": true, "locked": false, "suspended": false
    },
    "org_membership": {"permission": "admin", "compliance_auditor": false, "member_added_at": "2026-01-02 09:00:00 UTC"},
    "workspaces": [{"id": "4123456789012345678", "name": "Legal", "permission": "member", "member_added_at": "2026-01-03 09:00:00 UTC"}],
    "credentials": {"api_keys": 3, "oauth_sessions": 2},
    "logins": {
      "last_login": "2026-09-23 07:10:00 UTC", "window_days": 90, "count": 41,
      "recent": [{"created": "2026-09-23 07:10:00 UTC", "method": "sso", "ip": "203.0.113.7", "country": "US", "user_agent": "Mozilla/5.0 ..."}]
    },
    "last_activity": "2026-09-23 07:12:00 UTC"
  },
  "partial": [], "truncated": []
}

Response fields

FieldTypeDescription
report.user.managed_accountbooleantrue when the email is on a verified org SSO domain — predicts credential revocability above
report.org_membership.compliance_auditorbooleanWhether this member holds the auditor flag
report.workspacesarrayThis org's workspaces only, from the member's own workspace memberships
report.credentialsobjectCounts only (api_keys, oauth_sessions) — reach org_only|user_wide|mixed only. For rows, call List Org Credentials with user_id=
report.logins.recentarrayNewest 20. For full history, call events/search with event=user_login&calling_user_id= — see the Compliance & Audit events reference
report.last_activitystring or nullExcludes legal-hold events unless the viewer can manage holds
partial / truncatedarray of stringSection names (user|workspaces|credentials|logins|last_activity) that failed, or were built from a capped read

Error responses

ReasonHTTP
member_not_found404
compliance_access_required / scope_admin_required / plan_required403

Get Sharing Exposure Report

GET /current/org/{org_id}/reports/sharing/

Which workspaces have public shares/links or external (non-member, non-verified-domain) participants. Auth: admin+ or auditor. Enterprise.

Query parameters

ParameterTypeRequiredDefaultDescription
workspace_idstringNo—Switches to detail mode for that one workspace
only_exposedstringNofalsetrue | false (literal strings — any other value is a 406). List mode only: skip workspaces whose summary counts are all zero
limitintegerNo201–20 workspaces per page (list mode)
cursorstringNo—Opaque keyset cursor
countsstringNo"true""true" | "false" (literal strings). List mode only, ignored in detail mode. "false" skips the per-workspace exposure walk entirely — see below.

counts=false (list mode only). Returns workspace rows only (the same workspace block — id, name) with no summary block and no per-workspace exposure walk; only_exposed is ignored. Same auth, and the same limit/cursor paging, as the default. This is meant for building an admin-wide workspace picker — for example the ai_workspaces allowlist editor above — cheaply at org scale (a large org can have on the order of a thousand workspaces).

{"result": true,
 "workspaces": [{"workspace": {"id": "4123456789012345678", "name": "Legal"}}],
 "pagination": {"has_more": true, "next_cursor": "…", "page_size": 20}}

Response (200 OK) — list mode, default counts=true

{
  "result": true,
  "workspaces": [{
    "workspace": {"id": "4123456789012345678", "name": "Legal"},
    "summary": {"public_shares": 2, "public_file_links": 5, "external_members": 7, "pending_external_invites": 1}
  }],
  "pagination": {"has_more": true, "next_cursor": "…", "page_size": 20}
}

List mode returns counts only (no per-share/link/member rows). It also enforces an internal per-page read budget, so a page can come back shorter than limit — or even with a single workspace — while pagination.has_more is still true; keep paging on has_more rather than assuming a short page means the org is exhausted.

Detail mode (?workspace_id=…) adds shares[], file_links[], external_members[], pending_external_invites[] to the one workspace row:

{
  "workspace": {"id": "4123456789012345678", "name": "Legal"},
  "summary": {"public_shares": 1, "public_file_links": 1, "external_members": 1, "pending_external_invites": 2},
  "shares": [{
    "id": "5123456789012345678", "name": "Q3 data room", "category": "portal", "share_type": "send",
    "access_options": "Anyone with the link", "password_set": true, "expires": null,
    "creator": {"id": "9876543210987654321", "email_address": "creator@example.com"},
    "owner": {"id": "9876543210987654322", "email_address": "owner@example.com"},
    "member_count": 12,
    "external_members": [{"user_id": "9876543210987654323", "email_address": "vendor@example.com", "permission": "guest", "added_at": "2026-08-01 09:00:00 UTC"}],
    "pending_external_invites": [{"email_address": "invitee@example.com", "permission": "guest", "created": "2026-09-01 09:00:00 UTC"}]
  }],
  "file_links": [{
    "id": "6123456789012345678", "access_option": "anyone_with_link", "password_set": false,
    "expires": null, "creator": {"id": "9876543210987654321", "email_address": "creator@example.com"},
    "node_id": "2ltsuq4mjacuv7pgc5ydlxnsjwee4"
  }],
  "external_members": [{"user_id": "9876543210987654323", "email_address": "vendor@example.com", "permission": "member", "added_at": "2026-08-01 09:00:00 UTC"}],
  "pending_external_invites": [{"email_address": "invitee2@example.com", "permission": "member", "created": "2026-09-02 09:00:00 UTC"}]
}
FieldDescription
shares[].id / name / category / share_typeThe share's own identity fields
shares[].access_optionsThe existing share API's own text value — Only members of the Share or Workspace (default), Members of the Share, Workspace or Org, Anyone with a registered account, or Anyone with the link (a public link, counted in summary.public_shares). Not a machine token.
shares[].password_setboolean — the password itself is never echoed
shares[].creator / ownerMay differ after an ownership transfer; both are reported
shares[].member_countTotal member count on the share (org and external combined)
shares[].external_members[] / shares[].pending_external_invites[]External participants/invites scoped to this one share
file_links[].id / node_idThe file-link id and the file node (opaque id) it points to
file_links[].access_optionanyone_with_link | any_registered | named_people
file_links[].password_set / expires / creatorSame meaning as on a share
external_members[] / pending_external_invites[] (workspace-level)A non-org-member whose email is not on a verified org domain, at the workspace (not per-share) level

Error responses

ReasonHTTP
workspace_not_found (not in this org)404
compliance_access_required / scope_admin_required / plan_required403
invalid cursor406
A share, member, link or invitation read failed, or a person's external status cannot be determined — retryable; no partial page is returned and the cursor does not advance500

Export the Audit Log

GET /current/org/{org_id}/audit/export/

Stream the org's audit log as a file. Auth: admin+ or auditor, admin-scope credential. Enterprise.

Replaces any client-side paging loop over events/search. Not a 5,000-row export: it streams until the range is exhausted or a per-request row cap is hit (then hands back a cursor to continue).

Query parameters

ParameterTypeRequiredDescription
from / tostringYesYYYY-MM-DD, inclusive, UTC. Range ≤ 366 days.
formatstringNocsv (default) | jsonl
category, event, workspace_idstringNoSame names/meanings as events/search
user_idstringNoThe event's user (same meaning as events/search)
calling_user_idstringNoThe actor
cursorstringNoOpaque value from a truncation trailer — bound to org, filters, range and format; send with identical other parameters. Valid 7 days.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/audit/export/?from=2026-08-01&to=2026-08-31&format=csv" \
  -H "Authorization: Bearer {jwt_token}" -o audit.csv

Response: a streamed file, not a JSON envelope. Content-Type: text/csv; charset=UTF-8 or application/x-ndjson. Content-Disposition: attachment; filename="audit-<org>-<from>-<to>.<ext>".

Columns / JSONL keys: event_id, created, event, category, sub_category, calling_user_id, calling_user_email, event_user_id, workspace_id, share_id, object_id, ip, country, severity, metadata (metadata is a JSON string; every id is a string). CSV cells starting with = + - @ are prefixed with ' (formula-injection guard). Row visibility follows the caller's own audit-log rules (hold events owner/auditor only).

Stream end markers — the last line is always one of:

Error responses (before the stream starts, normal JSON envelope)

ReasonHTTPMeaning
(none)406from/to not a valid date, from later than to, or another parameter failed validation
range_too_large406params.max_days = 366
range_before_retention406params.earliest_date — refused, not clamped
cursor_invalid406Tampered, expired (>7 days), or parameters changed
workspace_not_found404workspace_id not in this org
export_in_progress429One export per org at a time. Always carries Retry-After (seconds until the held slot expires — worst case 30 min); released sooner on finish, failure, or a detected client disconnect.
compliance_access_required / scope_admin_required / plan_required403—

Event: org_audit_exported.


Grant/Revoke the Auditor Flag

POST /current/org/{org_id}/member/{user_id}/compliance-auditor/

Auth: the org owner only — not admins.

Request parameters

NameTypeRequiredDescription
enabledbooleanYes—

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/member/9876543210987654321/compliance-auditor/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "enabled=true"

Response (200 OK)

{"result": true, "user_id": "9876543210987654321", "compliance_auditor": true}

Enabling needs the Enterprise plan (on a current subscription); disabling always works, on any plan and on a lapsed subscription. Setting the current value, or targeting the owner (who already has every auditor power), is an idempotent 200 with no change. Removing the member from the org clears the flag; a plan lapse leaves it stored but inert; reviving an expired membership also clears it, so the flag never returns without the owner re-granting it.

Error responses (checked in this order)

ReasonHTTPMeaning
owner_required403Checked before the target is parsed or read, so a non-owner learns nothing about the target
scope_admin_required403Non-admin-scope credential
member_not_found404—
plan_required403Enable only — also needs a current subscription
successor_role_too_low406Enabling for a guest/view-rank member
membership_changed409The membership row changed while this request was in flight (e.g. a concurrent demotion) — re-read the member and retry

Events: org_compliance_auditor_changed, plus the existing membership_updated.


A legal hold preserves a workspace's or a member's content against destruction while litigation, an investigation, or another compliance need is active. Holds are managed by the org owner (any plan) or by an auditor (Enterprise) — org admins cannot manage holds, even though they can manage everything else (403 legal_hold_access_required). Placing a hold needs the Enterprise plan; listing, viewing and releasing an existing hold work on any plan, so a lapsed org can still lift its own holds. All four endpoints also need an admin-capable credential — a login session, or an API key/OAuth token with rwa scope on the org; any other credential gets 403 scope_admin_required.

What a hold does. A hold preserves, not freezes — members keep working normally. Deleting, moving, emptying trash, and closing a workspace or share all keep working exactly as before. What changes is permanent destruction: purge of trashed content, version pruning, automatic share expiry, and profile/account deletion are all held back for anything the hold covers, and quietly resume once the hold is released. A user never sees an error because of a hold — purging held content still reports success; the content is retained instead of destroyed. Held content, including deleted-but-retained bytes, keeps counting toward the org's billed storage.

Scope. A workspace hold covers the workspace itself, every one of its shares, and its e-sign envelopes — including ones created after the hold is placed. A person hold covers content the member authored across the org's workspaces, plus their own account. Person-hold coverage has two limits: files a cloud-sync import brought in with no resolvable owner, and copies of the person's files made by someone else, are attributed to the importer/copier and are not covered by a person hold — a workspace hold covers both of those regardless of who authored them.

Confidentiality. The legal_hold_created / legal_hold_released audit events never carry the hold's reason, and — unlike the rest of the audit log — they are visible only to the org owner or an entitled auditor, even in export and summarize, and only when the request itself carries a login session or an API key/OAuth token with an org admin-capable (rwa) scope on that same org — a user:*:rw-scoped key, or a key scoped to a different org, never sees them, whoever owns it. A search filtered to one of these two event types by a caller who does not qualify comes back as an empty page rather than an error — event names are matched exactly, so filtering on a case, whitespace, or accent variant of a hold-event name, or on any event value that is not itself a plain lowercase name, also returns an empty page rather than a broader match. They are never sent to a configured SIEM stream.

Hold object

{
  "id": "sabdqahjioknnh36puhwmjy5nluifp",
  "org_id": "1234567890123456789",
  "status": "active",
  "target_type": "workspace",
  "target": {"id": "4123456789012345678", "name": "Legal", "closed": false},
  "name": "Matter 2026-114",
  "reason": "Litigation hold per counsel",
  "created_by": {"id": "9876543210987654321", "email_address": "owner@example.com"},
  "created": "2026-09-23 16:37:29 UTC",
  "released_by": null,
  "released": null
}

For a person hold, target is {"id": "…", "email_address": "…", "first_name": "…", "last_name": "…"}. A target / created_by / released_by profile that no longer loads keeps its id with the display fields null (a workspace's closed reads true in that case) — a hold outlives the workspace it covers, since it can be placed on one that is already closed.


POST /current/org/{org_id}/legal-holds/

Auth: the org owner, or an auditor. Enterprise plan required.

Request parameters

NameTypeRequiredDescription
target_typestringYesworkspace | user
target_idstringYes19-digit numeric id of the workspace or member to hold
namestringYes1-255 characters, trimmed
reasonstringNoUp to 2000 characters. Never echoed to the audit-log event

A workspace target may already be closed, as long as it has not been purged. A person target must be a current org member. Several holds may target the same workspace or person at once.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/legal-holds/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "target_type=workspace" -d "target_id=4123456789012345678" \
  -d "name=Matter 2026-114" -d "reason=Litigation hold per counsel"

Response (200 OK)

result: true and legal_hold — the new hold, in the hold object shape shown above.

Error responses

ReasonHTTPMeaning
(field validation)406Missing or malformed target_type, target_id, name or reason
legal_hold_target_not_found404The workspace/member id is not in this org
legal_hold_target_not_member406A user target is not a current org member
legal_hold_access_required403Caller is neither the owner nor an entitled auditor
plan_required403Org is not on the Enterprise plan
(no reason)503Retryable — the hold store could not be reached. Nothing was placed.

Event: legal_hold_created (never carries reason; visible only to the owner/auditors).


GET /current/org/{org_id}/legal-holds/

Auth: the org owner, or an auditor. Any plan — a lapsed org can still see its holds.

Query parameters

ParameterTypeRequiredDefaultDescription
statusstringNoactiveactive | released | all
limitintegerNo1001-500
offsetintegerNo0—

Response (200 OK)

{
  "result": true,
  "legal_holds": [
    {
      "id": "sabdqahjioknnh36puhwmjy5nluifp",
      "org_id": "1234567890123456789",
      "status": "active",
      "target_type": "workspace",
      "target": {"id": "4123456789012345678", "name": "Legal", "closed": false},
      "name": "Matter 2026-114",
      "reason": "Litigation hold per counsel",
      "created_by": {"id": "9876543210987654321", "email_address": "owner@example.com"},
      "created": "2026-09-23 16:37:29 UTC",
      "released_by": null,
      "released": null
    }
  ],
  "pagination": {"total": 3, "limit": 100, "offset": 0, "has_more": false}
}

Errors: legal_hold_access_required (403).


GET /current/org/{org_id}/legal-holds/{hold_id}/

Auth: the org owner, or an auditor. Any plan.

Adds impact to the hold object. For a workspace hold: {"bytes": …, "files": …, "folders": …, "shares": …}, totalled across the workspace, its shares (a folder share that shares the workspace's own storage is not double-counted), and its e-sign envelopes — trashed and pending-deletion content included, the same figures billing uses. impact_available is true only when every part of the count succeeded; otherwise impact is null (retry later). A person hold always reports "impact": null, "impact_available": false. There is no separate preview endpoint — this is also where to check a hold's footprint right after placing it.

Response (200 OK)

{
  "result": true,
  "legal_hold": {
    "id": "sabdqahjioknnh36puhwmjy5nluifp",
    "org_id": "1234567890123456789",
    "status": "active",
    "target_type": "workspace",
    "target": {"id": "4123456789012345678", "name": "Legal", "closed": false},
    "name": "Matter 2026-114",
    "reason": "Litigation hold per counsel",
    "created_by": {"id": "9876543210987654321", "email_address": "owner@example.com"},
    "created": "2026-09-23 16:37:29 UTC",
    "released_by": null,
    "released": null,
    "impact": {"bytes": 123456789, "files": 1200, "folders": 80, "shares": 4},
    "impact_available": true
  }
}

Errors: legal_hold_not_found (404 — an unknown id and another org's id answer identically); legal_hold_access_required (403).


POST /current/org/{org_id}/legal-holds/{hold_id}/release/

Auth: the org owner, or an auditor. Never plan-gated — a lapsed org's owner can always lift a hold. No body.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/legal-holds/sabdqahjioknnh36puhwmjy5nluifp/release/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "legal_hold": {
    "id": "sabdqahjioknnh36puhwmjy5nluifp",
    "org_id": "1234567890123456789",
    "status": "released",
    "target_type": "workspace",
    "target": {"id": "4123456789012345678", "name": "Legal", "closed": false},
    "name": "Matter 2026-114",
    "reason": "Litigation hold per counsel",
    "created_by": {"id": "9876543210987654321", "email_address": "owner@example.com"},
    "created": "2026-09-23 16:37:29 UTC",
    "released_by": {"id": "9876543210987654321", "email_address": "owner@example.com"},
    "released": "2026-09-24 10:02:00 UTC"
  }
}

Releasing an already-released hold returns the same 200 idempotently and emits nothing further.

Errors: legal_hold_not_found (404); legal_hold_access_required (403).

Event: legal_hold_released (only on the release that actually changes the hold's status).

The hold is lifted lazily, not instantly — deferred destruction of the content it covered resumes automatically on its own schedule. There is nothing else to call; do not treat a hold that still shows released items pending cleanup as a bug.


Read org.capabilities.can_manage_legal_holds (see Get Org Details above) to decide whether to show this UI at all, rather than guessing from role alone.


SIEM Audit Stream (Enterprise plan)

One generic HMAC-signed HTTPS webhook per org, streaming the org's audit-log event set (the same events visible through Compliance & Audit above) to your own SIEM (Splunk, Datadog, or any HTTPS receiver). At-least-once delivery, batched, approximately ordered. Legal-hold events are never streamed — they stay owner/auditor-only in the audit log, the same restriction as everywhere else.

Semantics:

Who:

Get Audit Stream

GET /current/org/{org_id}/audit/stream/

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/audit/stream/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "stream": {
    "enabled": true, "state": "active",
    "url": "https://siem.example.com/fastio", "secret_masked": "****a1b2",
    "created": "… UTC", "updated": "… UTC", "updated_by": "…",
    "delivered_through": "2026-09-23 16:30:00 UTC", "events_delivered": 120394,
    "last_attempt_at": "… UTC", "last_success_at": "… UTC", "last_status_code": 200,
    "last_error_class": null, "consecutive_failures": 0, "gap_from": null, "gap_to": null
  }
}

Response fields

FieldTypeDescription
stream.statestringactive|paused_failing|paused_plan. A paused stream keeps enabled: true — show “paused” plus the reason
stream.last_error_classstring/nullnetwork|timeout|ssrf_blocked|http_4xx|http_5xx|dns_transient|plan_required|null
stream.gap_from / gap_tostring/nullSet when events aged out of retention while the stream was paused — there is a gap in what was ever delivered
stream.secret_maskedstringThe signing secret is never echoed in full — only the last 4 characters

Errors: 404 stream_not_configured (show the “Set up” state); 403 compliance_access_required.


Create or Update Audit Stream

POST /current/org/{org_id}/audit/stream/

Request parameters

NameTypeRequiredDescription
urlstringRequired on createHTTPS only, public host, no credentials in the URL, ≤2048 chars
enabledbooleanNo—

Create: returns the config plus "secret": "whsec_…" — shown once. Store it immediately; it cannot be retrieved again (only rotated). The stream starts delivering from “now”.

Update: secret is absent from the response. Re-enabling a paused or disabled stream resets the failure count and retries immediately.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/audit/stream/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "url=https://siem.example.com/fastio" -d "enabled=true"

Error responses

ReasonHTTPMeaning
stream_url_rejected406—
stream_exists409A concurrent create raced you
stream_secret_unavailable503Nothing was stored — retry later
plan_required403—
(field validation)406No url on create, or nothing to change

Event: org_audit_stream_updated.


Delete Audit Stream

DELETE /current/org/{org_id}/audit/stream/

Removes the stream configuration.

Response (200 OK)

{"result": true}

Errors: 404 stream_not_configured. Event: org_audit_stream_deleted.


Rotate Signing Secret

POST /current/org/{org_id}/audit/stream/rotate-secret/

Issues a new signing secret, effective immediately — there is no grace window, so update your receiver before or right after calling this.

Response (200 OK)

{"result": true, "secret": "whsec_…"}

Errors: same as Create or Update Audit Stream above. Event: org_audit_stream_updated with change: "secret_rotated".


Send a Test Delivery

POST /current/org/{org_id}/audit/stream/test/

Sends a single test delivery to the configured URL right now.

Response (200 OK)

{"result": true, "delivered": false, "status_code": 502, "error_class": "http_5xx", "latency_ms": 840}

— a failed delivery is a result, not an error; check delivered. Throttled to a small number of calls per org — 429 with Retry-After when you call it too soon after the last one.

The test delivery uses the same wire format and signature as a real one (below) and carries one synthetic record with event: "audit_stream_test". It is never stored as an event and never moves the stream's delivery position — have your receiver accept it (2xx) and otherwise ignore it.


Wire Format

POST <your url>
Content-Type: application/json

{"stream": "fastio.audit", "org_id": "…", "delivery_id": "…", "sent_at": "… UTC", "events": [ … ]}

Headers: X-Fastio-Delivery-Id; X-Fastio-Timestamp (unix seconds); X-Fastio-Signature: v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw request body>")>.

Verifying the signature: recompute HMAC-SHA256(your_secret, timestamp + "." + raw_body) over the raw (unparsed) request body, hex-encode it, and compare to the value after v1= using a constant-time comparison. Reject the request if the timestamp is more than 5 minutes old (also guards against replay). Do not trust an unsigned or mis-signed delivery.

Event record fields: event_id, event, category, sub_category, severity, created, org_id, workspace_id, share_id, actor_user_id, subject_user_id, object_id, description, metadata, ip, country (every id is a string). An org_security_alert record carries no event user and no actor, so its top-level subject_user_id and actor_user_id are both null — read the alert's subject from metadata.subject_user_id instead.

Your receiver should: verify the signature over the raw body; reject stale timestamps; dedupe on event_id (delivery is at-least-once and a retry can regroup events); treat ordering as approximate; respond with any 2xx to acknowledge (redirects are not followed — respond directly).


Security Alerts (Enterprise plan)

Proactive notification — an audit-log event plus an email — when one of a handful of security-shaped patterns happens in the org: a new-country sign-in, a geo/IP policy block, an unusually large burst of deletions or downloads by one person, a new credential that can reach the org, or the audit stream above pausing itself. Enterprise plan only.

Setting: the security_alerts field on Update Organization above (admin+). Unset means every alert is on and compliance auditors are included as recipients — the default for an Enterprise org that has never configured this. enabled: [] turns every alert off; clear with "" to return to the default.

Alert types:

AlertFires when
login_new_countryA member signs in from a country with no prior sign-in in the org's retained history (never for an unresolvable location)
geo_policy_blockA request was refused by the org's geo/IP access policy (see Access Policy (Geo / IP Restrictions) above)
mass_deleteOne member deletes, purges, or empties trash on an unusually large number of files/folders in a short window
mass_downloadOne member downloads, zips, or directly reads an unusually large number of files in a short window
credential_createdA new API key or OAuth grant is created that can reach this org (any wildcard-scoped credential counts)
audit_stream_pausedThe SIEM audit stream (see above) auto-pauses after sustained delivery failure — always on, cannot be disabled

Repeated alerts of the same kind are throttled so a single ongoing pattern does not flood you with duplicates, and each alert type is capped per org per day — once the cap is hit, further alerts of that kind are suppressed for the rest of the day and the next alert's notification says so.

Output:

In-app: GET /current/events/search/?org_id=…&visibility=external_audit_log&event=org_security_alert (add &acknowledged=false for an unread badge). Acknowledge with POST /current/event/{event_id}/ack/. Acknowledgement is per viewer — one admin acknowledging an alert does not clear it for other admins or auditors. A compliance auditor who is not an admin may open event/{event_id}/details/ and acknowledge an org_security_alert row the same as an admin can.

Errors: 406 params[] {name: "security_alerts"} for an unknown alert name in the setting write; 403 plan_required when the write would turn an alert on, or turn auditors on, on a non-Enterprise org (turning alerts off, removing auditors, or clearing is allowed on any plan).

Event: org_security_alert per alert. org_updated's policy_changes also gains security_alerts ({before, after}) when the setting itself changes.


Organization Discovery

List Internal Orgs

GET /current/orgs/list/

Auth required. Lists orgs where the user is a direct member (member: true). Paginated: optional limit (1-500, default 100) and offset query parameters; the response carries pagination (total, limit, offset, has_more).

Non-admin/non-owner members only see orgs with active subscriptions; admins and owners always see their orgs.

curl example

curl -X GET "https://api.fast.io/current/orgs/list/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "orgs": [
    {
      "id": "1234567890123456789",
      "domain": "acme-corp",
      "name": "Acme Corporation",
      "description": "Leading provider of innovation",
      "logo": "https://assets.fast.io/org/logo.png",
      "accent_color": {"color": "#0066CC", "opacity": 100},
      "closed": false,
      "suspended": false,
      "subscriber": true,
      "user_status": "joined",
      "member": true
    }
  ],
  "pagination": {"total": 1, "limit": 100, "offset": 0, "has_more": false}
}

Response fields

FieldTypeDescription
orgsarrayArray of organization objects
orgs[].idstring19-digit numeric organization ID
orgs[].domainstringURL-safe subdomain
orgs[].namestring/nullDisplay name
orgs[].descriptionstring/nullDescription
orgs[].logostring/nullLogo asset URL
orgs[].accent_colorobject/nullBrand color ({color, opacity})
orgs[].closedbooleanWhether org is closed
orgs[].suspendedbooleanWhether org is suspended
orgs[].subscriberbooleanWhether org has an active subscription
orgs[].user_statusstring"joined" or "available"
orgs[].memberbooleanAlways true for this endpoint

Subscription filtering

User RoleBehavior
OwnerAlways sees the org
AdminAlways sees the org
MemberOnly sees the org if it has an active subscription

List External Orgs

GET /current/orgs/list/external/

Auth required. Lists orgs where the user has access only through workspace membership (member: false). Paginated: optional limit (1-500, default 100) and offset query parameters; the response carries pagination (total, limit, offset, has_more).

curl example

curl -X GET "https://api.fast.io/current/orgs/list/external/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "orgs": [
    {
      "id": "1234567890123456780",
      "domain": "partner-corp",
      "name": "Partner Corporation",
      "description": "External partner organization",
      "logo": null,
      "accent_color": {"color": "#FF6600", "opacity": 100},
      "closed": false,
      "suspended": false,
      "subscriber": true,
      "user_status": "available",
      "member": false
    }
  ],
  "pagination": {"total": 1, "limit": 100, "offset": 0, "has_more": false}
}

Response fields

FieldTypeDescription
orgsarrayArray of external organization objects
orgs[].user_statusstringAlways "available" for external orgs
orgs[].memberbooleanAlways false for this endpoint

List All Orgs

GET /current/orgs/all/

Auth required. Lists all accessible orgs (joined + invited). Paginated: optional limit (1-500, default 100) and offset query parameters; the response carries pagination (total, limit, offset, has_more).

curl example

curl -X GET "https://api.fast.io/current/orgs/all/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "orgs": [
    {
      "id": "1234567890123456789",
      "domain": "acme-corp",
      "name": "Acme Corporation",
      "description": "Leading provider of innovation",
      "logo": "https://assets.fast.io/org/logo.png",
      "accent_color": {"color": "#0066CC", "opacity": 100},
      "closed": false,
      "suspended": false,
      "user_status": "joined"
    }
  ],
  "pagination": {"total": 1, "limit": 100, "offset": 0, "has_more": false}
}

Response fields

FieldTypeDescription
orgs[].user_statusstring"joined" (already a member) or "available" (pending invitation)

List Available Orgs

GET /current/orgs/available/

Auth required. Lists orgs available to join (not yet joined). Excludes orgs the user is already a member of.

curl example

curl -X GET "https://api.fast.io/current/orgs/available/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "orgs": [
    {
      "id": "1234567890123456782",
      "domain": "new-company",
      "name": "New Company",
      "description": "An org you can join",
      "logo": null,
      "accent_color": {"color": "#FF6600", "opacity": 100},
      "closed": false,
      "suspended": false
    }
  ]
}

Check Domain Availability

GET /current/orgs/check/domain/{domain_name}

Auth required. Checks if an org domain name is available for use.

Path parameters

NameTypeRequiredDescription
{domain_name}stringYesThe domain name to check for availability.

curl example

curl -X GET "https://api.fast.io/current/orgs/check/domain/acme-corp" \
  -H "Authorization: Bearer {jwt_token}"

Response (202 Accepted) — domain available

{
  "result": true
}

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"An invalid name was supplied."Domain format is invalid
1658 (Not Acceptable)406"The supplied name is restricted."Domain is reserved
1658 (Not Acceptable)406"The supplied name is already in use."Domain is taken

List Industries

GET /current/orgs/industries/

Auth required. Returns available industry types for org profiles.

curl example

curl -X GET "https://api.fast.io/current/orgs/industries/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "unspecified": {
    "title": "Unspecified",
    "description": "No specific industry or sector."
  },
  "technology": {
    "title": "Technology",
    "description": "Companies that develop or provide software, hardware, and IT services."
  },
  "healthcare": {
    "title": "Healthcare",
    "description": "Organizations providing medical services, healthcare management, and patient care."
  },
  "financial": {
    "title": "Financial Services",
    "description": "Businesses offering banking, investment, and financial management services."
  }
}

Response fields

FieldTypeDescription
{key}objectKeyed by the machine-readable industry identifier (use this key in create/update requests). Every industry is returned, starting with unspecified.
{key}.titlestringHuman-readable display name
{key}.descriptionstringBrief description of the industry category

Billing

The unsubscribed tier is identified as unpaid. It was previously reported as free; clients should accept both for now and treat them as the same tier. Where a plan identifier is accepted as INPUT, free is still accepted and resolves to unpaid. unpaid is not a purchasable plan: it never appears in GET /current/org/billing/plan/list/, and subscribing to it is refused.

Where the identifier appears for an org with no active subscription, and where it does not:

SurfaceWhat an unsubscribed org returns
The org's raw plan field (org details and the org list)"unpaid"
POST /current/org/{org_id}/billing/ → billing_status.current_planThe full plan object, with name: "unpaid", title: "Unpaid", category: "unpaid"
GET /current/org/{org_id}/billing/details/ → billing_status.current_plan{} — an empty object, not a plan named unpaid. This endpoint fills current_plan only for an org whose subscription is currently active
billing_status.previous_planThe full plan object, with name: "unpaid", when the previous plan was the unsubscribed tier

Do not read an empty current_plan as "no plan" or as an error. On GET .../billing/details/ it is the normal shape for an org that is not a current subscriber. To learn which tier such an org is on, read the org's plan field rather than this object.

Preview a Plan Change

GET /current/org/{org_id}/billing/preview/?billing_plan={plan}

Auth required. Admin or above. Returns what a plan change will cost before it is made. Read-only: creates no invoice and changes no subscription.

Use this before POST /current/org/{org_id}/billing/ whenever the org already has a subscription. A change that increases committed spend — a higher tier, or monthly to annual on the same tier — is invoiced immediately rather than at the next cycle, so the card is charged in the same interaction the customer confirms. Show them this figure first.

Request parameters

NameTypeRequiredDescription
billing_planstringYesTarget plan ID (a valid, currently-offered paid plan)

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/preview/?billing_plan=enterprise_v2_monthly" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "preview": {
    "amount_due_cents": 20132,
    "currency": "usd",
    "proration_date": 1756670400,
    "source_plan": "business_v3_monthly",
    "target_plan": "enterprise_v2_monthly",
    "spend_increasing": true,
    "ends_trial": false
  }
}
FieldTypeDescription
preview.amount_due_centsintegerTotal due today, in cents, including tax. Divide by 100 before display.
preview.currencystringISO currency code
preview.proration_dateintegerUnix timestamp this quote was priced at. Pass it back on the change request so the swap prices the same instant — otherwise the customer can be charged a different figure from the one they agreed to, simply because time passed between the two calls.
preview.source_planstringPlan the subscription currently holds
preview.target_planstringPlan being previewed
preview.spend_increasingbooleantrue when the change increases committed spend and will therefore invoice immediately
preview.ends_trialbooleantrue when confirming also ends a running trial. The copy must say so — “this ends your trial and charges $X today” reads very differently from “this charges $X today”.

Errors

Error CodeHTTP StatusCause
10176406billing_plan missing or not a currently-offered plan
10737429The billing system is busy with another change for this org — retry shortly
10764406No preview is available — most commonly because the org has no subscription yet, and a first subscription is not a “change” to price. ⚠ Do NOT assume money is due. A first subscription charges $0 today when the plan offers a trial (pricing.free_days > 0) and the org is eligible (billing_status.free_trial_eligible); it charges the plan's list price only when neither holds. Decide from those two fields — not from the absence of a preview, and not from a local upgrade/downgrade classifier, which will read free → paid as a spend increase and tell a customer starting a trial that they are being charged.

Create or Update Subscription

POST /current/org/{org_id}/billing/

Auth required. Admin or above. Creates or updates the org's billing subscription.

Changing plan while a cancellation is scheduled cancels the pending cancellation: the subscription continues on the new plan. (A plan change still awaiting card authentication cancels it once the payment completes.) A plan change that may charge immediately may be refused with error 10777 when the scheduled cancellation takes effect within the next day: reactivate the subscription first, then change plan.

Request parameters

NameTypeRequiredDescription
billing_planstringNoTarget plan ID (must be a valid, currently-offered paid plan, e.g., "starter_monthly", "business_v3_monthly", "enterprise_v2_monthly"). Each plan also has an annual variant (e.g., "business_v3_annual"). Plan IDs that are not currently offered are rejected here (existing orgs on them are unaffected).
proration_dateintegerNoThe preview.proration_date from a prior GET .../billing/preview/ call. Pass it whenever you showed the customer a figure, so the invoice prices the instant they were quoted rather than drifting by however long they spent on the confirm dialog — a spend-increasing change collects immediately, so that drift is a real charge. Validated server-side. A quote in the future, or older than one billing period, is REFUSED with error 10765 rather than silently ignored — you showed the customer a figure, so repricing it quietly would charge them something they never agreed to. Recovery: request a fresh preview and confirm again, which shows them the correct current amount.
checkoutstringNotrue (also 1, yes, on) to start a new subscription on a Stripe-hosted checkout page instead of confirming a SetupIntent in your own card form. Any other value, or omitting it, keeps the card-form flow. Only applies to an org with no active paid subscription and no unpaid subscription; a plan change or an unpaid-subscription recovery responds exactly as without it. See Hosted checkout below.
success_urlstringWith checkout=trueWhere the checkout page sends the browser after the card is accepted. Must be an https URL on the Fastio web app's own site (fast.io), with no user-info, backslashes or whitespace, at most 1024 characters. Existing query parameters and the fragment are kept; a session_id query parameter is added (replacing any you sent) and filled with the checkout session id on redirect.
cancel_urlstringWith checkout=trueWhere the checkout page sends the browser if the customer backs out. Same rules as success_url; nothing is added to it.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/billing/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "billing_plan=business_v3_monthly"

Response (201 Created) — new subscription

{
  "result": true,
  "billing_status": {
    "active": false,
    "free_trial_eligible": true,
    "current_plan": { "...": "..." },
    "customer": { "...": "..." },
    "subscription": { "...": "..." },
    "setup_intent": {
      "id": "{setup_id}",
      "client_secret": "{setup_id}_secret",
      "status": "requires_payment_method"
    },
    "payment_intent": [],
    "payment_recovery": [],
    "public_key": "{public_key}"
  }
}

Every field on this response is nested under billing_status — there is no root-level subscription, is_active, or is_trial_eligible.

This response shape is the same whether or not a trial applies. Confirm the billing_status.setup_intent.client_secret with the customer's card. No money moves at that moment — the SetupIntent is a $0 card capture that also performs any 3-D Secure authentication up front.

The card is inspected before any charge: a card refused by the funding rules (see payment-method/precheck/) does not produce a subscription, and the customer's billing address is taken from the card, so the first invoice is taxed correctly.

Hosted checkout (checkout=true) — Response (201 Created)

Send checkout=true with success_url and cancel_url to let a Stripe-hosted page take the card instead of your own form:

curl -X POST "https://api.fast.io/current/org/1234567890123456789/billing/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "billing_plan=business_v3_monthly" \
  -d "checkout=true" \
  -d "success_url=https://fast.io/billing/done" \
  -d "cancel_url=https://fast.io/billing"
{
  "result": true,
  "billing_status": {
    "active": false,
    "free_trial_eligible": true,
    "current_plan": { "...": "..." },
    "customer": { "...": "..." },
    "subscription": { "...": "..." },
    "setup_intent": [],
    "payment_intent": [],
    "payment_recovery": [],
    "public_key": "{public_key}",
    "checkout": {
      "id": "{checkout_session_id}",
      "url": "https://{payment_provider_checkout_host}/...",
      "expires_at": "2026-10-01 14:03:22 UTC",
      "trial": true,
      "trial_days": 30
    }
  }
}
FieldTypeDescription
billing_status.checkout.idstringCheckout session id. The same value is substituted into success_url's session_id parameter on the way back.
billing_status.checkout.urlstringRedirect the browser here.
billing_status.checkout.expires_atstringWhen the checkout page stops accepting the card (YYYY-MM-DD HH:MM:SS UTC).
billing_status.checkout.trialbooleanWhether this checkout starts a free trial.
billing_status.checkout.trial_daysintegerTrial length in days; 0 when no trial applies.

setup_intent is empty in this response — there is nothing to confirm in your own page. checkout.trial and checkout.trial_days state whether this checkout grants a free trial (an annual plan, for example, has none even when free_trial_eligible is true); when the org owner's trial eligibility cannot be verified, no trial is offered.

The hosted page accepts cards only and requires a billing address. Beside the submit button it states either the free trial (up to the plan's trial days, or until the included trial credits are used; nothing is charged to start it; then the plan price per month plus usage and applicable tax, unless cancelled) or the amount charged today (the plan price plus applicable tax, billed monthly or annually, with usage billed monthly). No fixed trial end date is promised.

Starting checkout again for the same plan, trial and return URLs while the open page still has at least 10 minutes left returns that same session, so a double-submit is safe. Any other start replaces it: earlier open checkout pages are closed and a new one is created. Starting the card-form flow (without checkout) also closes any open checkout page, so an old checkout tab cannot complete.

Neither this request nor the redirect back creates the subscription. It is created shortly afterwards, once the card is confirmed — exactly as in the card-form flow, including the trial/no-trial charging rules and the card funding rules. After the browser returns to success_url, poll GET /current/org/{org_id}/billing/details/:

  1. billing_status.current_plan.name equals the plan you started → done.
  2. billing_status.payment_recovery is non-empty → send the customer to payment_recovery.invoice.hosted_invoice_url to complete payment.
  3. billing_status.refusal_reason is non-null → the card was refused and no subscription was created; let the customer start checkout again with a different card.
  4. Otherwise keep polling, up to your own timeout.

Response (202 Accepted) — subscription updated. A change that settles right away (a downgrade, or an upgrade that needs no additional authentication) returns 202 with no body.

Response (202 Accepted) — payment action required. When a plan change may raise a charge and the card needs additional authentication (3-D Secure), the org's subscription stays on its current plan and the response carries a payment_recovery descriptor for the pending change:

{
  "result": true,
  "billing_status": {
    "current_plan": { "name": "business_v3_monthly", "...": "..." },
    "payment_recovery": {
      "recoverable": true,
      "status": "active",
      "plan": "enterprise_v2_monthly",
      "invoice": {
        "id": "{invoice_id}",
        "status": "open",
        "amount_due": 20132,
        "currency": "usd",
        "hosted_invoice_url": "https://{payment_provider_host}/i/.../{invoice_id}"
      },
      "payment_intent": {
        "id": "{payment_intent_id}",
        "client_secret": "{payment_intent_id}_secret",
        "status": "requires_action",
        "requires_action": true
      }
    },
    "payment_intent": { "...": "same object as billing_status.payment_recovery.payment_intent" }
  }
}

billing_status.current_plan still names the plan the org is on today; payment_recovery.plan is the target of the pending change. payment_recovery.status here reports the subscription's own live status (active or trialing, not a fixed “pending” value) — recognize a pending upgrade as payment_recovery whose status is active or trialing, together with plan (the target). An unpaid subscription's recovery instead carries incomplete, past_due or unpaid. Confirm payment_recovery.payment_intent.client_secret with Stripe.js (a new card may be used) — the plan itself changes only once payment settles, moments later and asynchronously. payment_recovery disappearing on a later read is NOT itself a success or failure signal — it clears the instant the charge succeeds, before the plan changes. After a successful confirm, poll GET .../billing/details/ until billing_status.current_plan.name equals the plan you confirmed; that is the actual promotion. If the challenge is abandoned, the pending change expires after up to about 23 hours (sooner if the current billing period ends first). On an account that is current on its billing, abandoning the challenge simply leaves the org on its existing plan — no past-due status, no dunning. An account that is already behind on payment, or on an older subscription still on the prior billing mechanics, may still become past-due if the authentication is abandoned.

Re-posting the same target plan while a change is pending returns the same payment_intent rather than starting a new one — get a fresh GET .../billing/preview/ quote first (or omit proration_date), since a stale quote is refused. Posting a different plan, a downgrade, or the org's current plan first cancels the pending change; posting the current plan on its own just cancels it and leaves the subscription as it was.

Response (200 OK) — payment recovery: if the org already has an UNPAID subscription (status incomplete, past_due, or unpaid — e.g. a prior card was declined), no new subscription or setup intent is created. The response instead carries a non-empty billing_status.payment_recovery object (see Get Billing Details below for its shape); confirm its nested payment_intent.client_secret with a (new) card to pay the existing open invoice. While a subscription is in this state a plan switch is not applied — the outstanding invoice must be settled first, after which the plan can be changed once the subscription is active. (This is a genuinely unpaid subscription, distinct from the pending-upgrade case above — a pending upgrade's own invoice does not block a further plan change; posting a different plan simply cancels it, per the previous paragraph.)

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"An invalid plan was supplied."Plan ID not recognized
1605 (Invalid Input)406"That plan is not available for self-service subscription. Contact sales."The plan is a contact-sales plan, not offered for self-service subscription
1605 (Invalid Input)406"Cannot create subscription for the unpaid plan. Please select a paid plan."Tried to subscribe to the unpaid plan (an org with no active subscription)
1658 (Not Acceptable)406"An error occurred updating your subscription..."Subscription update failed
1658 (Not Acceptable)406"Your subscription is scheduled to cancel soon..."A plan change that may charge immediately may be refused while the subscription is scheduled to cancel within the next day (error code 10777). Reactivate the subscription first, then change plan.
1658 (Not Acceptable)406"An error occurred creating the payment intent..."Intent creation failed
1605 (Invalid Input)406"A valid success_url and cancel_url on this site are required."checkout=true with a missing or invalid success_url / cancel_url (error code 143365)
1658 (Not Acceptable)406"An error occurred starting checkout, please try again."The hosted checkout page could not be started (error code 196606)

Schedule Subscription Cancellation

DELETE /current/org/{org_id}/billing/

Auth required. Owner only. Schedules the org's subscription to cancel at the end of the current billing period. The customer keeps full access until cancel_at. Use PUT (below) to reverse the schedule before cancel_at is reached. A trial is scheduled to end at its trial end, so it is never charged.

One exception: a trial whose trial period has already ended but that has not yet been billed is cancelled immediately instead of at period end. Nothing is charged and nothing is prorated, access ends at once, and the response is already_cancelled with no cancel_at. It cannot be reversed with PUT; a new subscription is required.

Request Parameters

ParameterTypeRequiredDescription
reasonstringNoWhy the customer is cancelling: one of price, not_working, missing_feature, other. Passed on to the payment provider with the cancellation; not stored or returned by this API.

curl example

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/billing/" \
  -H "Authorization: Bearer {jwt_token}"

Optionally include reason to tell us why:

curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/billing/?reason=price" \
  -H "Authorization: Bearer {jwt_token}"

Response (202 Accepted)

{
  "result": true,
  "status": "scheduled_cancellation",
  "message": "Your subscription is scheduled to end at the close of the current billing period.",
  "cancel_at": 1735689600,
  "cancel_at_period_end": true,
  "closed": false
}

If a cancellation has already been scheduled (or already executed), or a trial whose trial period had already ended was just cancelled immediately:

{
  "result": true,
  "status": "already_cancelled",
  "message": "Subscription is already cancelled"
}

Response fields

FieldTypeDescription
statusstring"scheduled_cancellation" or "already_cancelled"
messagestringHuman-readable status message
cancel_atinteger/nullUnix timestamp when access will end. null only if the subscription record could not be re-read after scheduling.
cancel_at_period_endbooleanAlways true on a successful schedule
closedbooleanAlways false for the scheduled-cancel flow — the org remains open until cancel_at

Notes

Error responses

Error CodeHTTP StatusMessageCause
1683 (Resource Missing)404"No subscription was found to cancel."Org is not a subscriber
1654 (Internal Error)500"Your subscription failed to be canceled..."Cancellation failed
1605 (Invalid Input)400"An invalid cancellation reason was supplied. Use one of: price, not_working, missing_feature, other."reason was supplied but is not one of the allowed values

Reactivate Subscription

PUT /current/org/{org_id}/billing/

Auth required. Owner only. Reactivates a subscription whose cancellation was scheduled via DELETE /current/org/{org_id}/billing/ but has not yet executed. Clears cancel_at_period_end so the subscription renews normally.

curl example

curl -X PUT "https://api.fast.io/current/org/1234567890123456789/billing/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "status": "reactivated",
  "message": "Your subscription has been reactivated and will renew at the end of the current billing period.",
  "current_period_end": 1735689600,
  "cancel_at_period_end": false
}

Response fields

FieldTypeDescription
statusstringAlways "reactivated" on success
messagestringHuman-readable status message
current_period_endinteger/nullUnix timestamp of the next renewal
cancel_at_period_endbooleanAlways false on success

Notes

Error responses

Error CodeHTTP StatusMessageCause
1683 (Resource Missing)404"No active subscription was found to reactivate."Org is not currently a subscriber
1654 (Internal Error)500"Your subscription could not be reactivated, please contact support."Reactivation failed

Get Billing Details

GET /current/org/{org_id}/billing/details/

Auth required. Admin or above. Returns subscription/billing details.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/details/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "billing_status": {
    "active": true,
    "free_trial_eligible": false,
    "current_plan": { "...": "..." },
    "customer": { "...": "..." },
    "subscription": { "...": "..." },
    "setup_intent": { "...": "..." },
    "payment_intent": { "...": "..." },
    "payment_recovery": { "...": "..." },
    "public_key": "{public_key}",
    "refusal_reason": null
  }
}

Every field on this response is nested under billing_status — there is no root-level subscription, is_active, or is_trial_eligible. billing_status.previous_plan is present only when the subscription is cancelled (see below).

Response fields

FieldTypeDescription
billing_status.subscriptionobjectPayment provider subscription details
billing_status.customerobjectPayment provider customer details
billing_status.setup_intentobject, or [] when noneActive setup intent if exists
billing_status.setup_intent.trialbooleanWhether the intent was minted with a trial. Capped to false whenever billing_status.free_trial_eligible is false — including an intent minted earlier while the org was still eligible — so it never advertises a trial the org is not currently eligible for.
billing_status.payment_intentobject, or [] when noneActive payment intent if exists
billing_status.payment_recoveryobject, or [] when emptyPresent (non-empty) either when an existing subscription's invoice is unpaid — status incomplete, past_due, or unpaid (e.g. a declined first payment) — or when an active/trialing subscription has a plan change pending additional card authentication. For a pending plan change, status reports the subscription's own live status (active or trialing, not a fixed “pending” value) and plan is the pending target — read the two together to recognize this case. A third case: a trialing subscription whose early trial end (the usage-triggered conversion to paid) is waiting on card authentication or a successful charge — status is trialing and plan is the org’s CURRENT plan; completing the payment ends the trial and starts the paid period, and if it is not completed within about 23 hours the trial simply continues. Contains recoverable (bool), status, plan, subscription {id,status}, invoice {id,status,amount_due,currency,hosted_invoice_url}, and payment_intent {id,client_secret,status,requires_action}. invoice.hosted_invoice_url is populated in every case — a Stripe-hosted page where the customer can pay or complete authentication without signing in. The client completes payment by confirming that PaymentIntent's client_secret with a (new) card. On a reload mid-pending-change, this same object lets the client finish the challenge in place. An empty array [] when there is nothing to recover.
billing_status.activebooleanWhether subscription is currently active
billing_status.free_trial_eligiblebooleanWhether a trial is available for this org. false once this org has ever subscribed, once this org is not the owner's first organization (permanent — a trial is only ever available on a user's first org, so an owner can still be true on that first org even while owning others), or once the owner has ever started a free trial on any organization (permanent, lifetime — one trial per user, ever).
billing_status.current_planobjectFull plan-details object for the org's current plan. Filled only while the subscription is ACTIVE — an org that is not a current subscriber returns {} here, NOT a plan named unpaid. That is the normal shape for an unsubscribed org, not an error; read the org's plan field to learn its tier. (The POST /current/org/{org_id}/billing/ response differs: it returns the full object with name: "unpaid".)
billing_status.previous_planobjectFull plan-details object for the org's previous plan. Present only when the subscription is cancelled. Reports name: "unpaid" when the previous plan was the unsubscribed tier.
billing_status.public_keystringPayment provider publishable key
billing_status.refusal_reasonstring/nullWhy the most recent card submitted was refused, when it was refused by the card funding rules and no subscription was created: "prepaid_card_refused" (prepaid cards) or "debit_card_amount_refused" (debit cards on plans charging more than $250). null otherwise, and always null while the org has an active paid subscription. Kept for about an hour; a refusal from an earlier attempt is not reported once a new hosted checkout is started. Returned by this endpoint only.

Get Credit Usage

GET /current/org/{org_id}/billing/usage/limits/credits/

Auth required. Admin or above. Returns credit consumption and limits.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/usage/limits/credits/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "credit_limits_enabled": false,
  "in_trial": false,
  "trial_auto_convert_at": 0,
  "free_credits_tracking": true,
  "free_org_mode": false,
  "org_id": "1234567890123456789",
  "plan": "starter_monthly",
  "over_free_allowance": false,
  "usage": {
    "credits_used": 1200,
    "free_credit_allowance": 100000,
    "credits_remaining": 98800,
    "usage_percentage": 1.2
  },
  "period": {
    "start": "2025-01-15 10:00:00 UTC",
    "end": "2025-02-14 10:00:00 UTC",
    "days_total": 30,
    "days_elapsed": 5,
    "days_remaining": 25
  },
  "renewal": {
    "interval_days": 30,
    "next_renewal": "2025-02-14 10:00:00 UTC"
  },
  "run_rate": null
}

The example is a paid plan outside its trial. Paid plans bill usage beyond the monthly allowance as overage, so credit_limits_enabled is false and credits_remaining tracks the included allowance rather than a ceiling. During a trial, or on a legacy plan that stops at its allowance, credit_limits_enabled is true and usage is held at the monthly allowance. A trial with a cancellation scheduled never converts to paid: usage is held at the trial's own credit allowance instead (reported as usage.free_credit_allowance), trial_auto_convert_at is 0, and in_trial stays true past the trial's end until the plan ends (unless the trial was already billed for its first term). Orgs without a paid plan, and legacy plans with a fixed credit cap, receive a capped shape instead: over_limit, usage.credit_limit and trial in place of the allowance fields.

Response fields

FieldTypeDescription
credit_limits_enabledbooleantrue when usage is held at an allowance (a trial, a legacy plan that stops at its allowance, or a capped plan); false when overage is billed
in_trialbooleanPaid plans: whether the org is in its free trial
trial_auto_convert_atintegerPaid plans in a trial: the credit usage at which the trial converts to paid; 0 otherwise, including a trial with a cancellation scheduled (it never converts)
free_credits_trackingbooleanPaid plans: true; usage is tracked against the included allowance
free_org_modebooleantrue for orgs without a paid plan (unpaid credit model, including new unsubscribed orgs); does not imply credits are available
over_free_allowancebooleanPaid plans: whether usage has reached the allowance in usage.free_credit_allowance (the included monthly allowance, or the trial's credit allowance for a trial with a cancellation scheduled)
over_limitbooleanCapped plans only: whether the org has exceeded its credit limit
usage.credits_usedintegerCredits consumed in the current period
usage.free_credit_allowanceintegerPaid plans: credits included per period; for a trial with a cancellation scheduled, the trial's credit allowance, where usage stops
usage.credit_limitintegerCapped plans only: total credits available per period
usage.credits_remainingintegerCredits remaining in the current period
usage.usage_percentagenumberPercentage of credits used
period.startstringStart of the current billing period (YYYY-MM-DD HH:MM:SS UTC)
period.endstringEnd of the current billing period (YYYY-MM-DD HH:MM:SS UTC)
period.days_totalintegerTotal days in the period
period.days_elapsedintegerDays elapsed since period start
period.days_remainingintegerDays remaining until renewal
renewal.interval_daysintegerDays between credit renewals
renewal.next_renewalstring/nullNext credit renewal timestamp (YYYY-MM-DD HH:MM:SS UTC), or null
run_rateobject/nullUsage rate projections (shown after 25% of period or credits used)
trialobject/nullCapped plans only: trial info if applicable

Credit costs: 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), cloud sync (1 per 1,000 objects scanned per sync, minimum 1 per sync), AI index (100 per 1,000 indexed vectors, sampled daily and charged on the period average, so a corpus you keep for a month costs 100 per 1,000 vectors for that month, not per day).


List Billable Members

GET /current/org/{org_id}/billing/usage/members/list/

Auth required. Admin or above. Paginated.

Query parameters

NameTypeDefaultDescription
limitinteger1001-500
offsetinteger0Items to skip

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/usage/members/list/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "billable_members": {
    "1234567890123456789": {
      "id": "1234567890123456789",
      "account_type": "human",
      "email_address": "user@example.com",
      "parents": {
        "9876543210987654321": {
          "permission": "member",
          "date_joined": "2024-01-15 10:30:00 UTC"
        }
      }
    }
  },
  "pagination": {"total": 1, "limit": 100, "offset": 0, "has_more": false}
}

Response fields

FieldTypeDescription
billable_membersobjectBillable member objects keyed by user ID (an object, not an array); [] when there are none
billable_members.{user_id}.idstring19-digit user ID
billable_members.{user_id}.account_typestring"human" or "agent"
billable_members.{user_id}.email_addressstringUser's email
billable_members.{user_id}.parentsobjectMap of workspace IDs to {permission, date_joined}
paginationobjecttotal, limit, offset, has_more

Get Usage Meters

GET /current/org/{org_id}/billing/usage/meters/list/

Auth required. Admin or above. Returns detailed usage breakdown by meter.

Query parameters

NameTypeRequiredDefaultDescription
meterstringYes—Meter type. One of: storage, bandwidth, users, tokens, credits, doc_pages_ingested, images_ingested, video_seconds_ingested, audio_seconds_ingested, video_seconds_converted, audio_seconds_converted, conversions, signatures, cloud_sync, ai_index. Any other value is refused with 406
start_timestring (datetime)No30 days agoStart of time range, YYYY-MM-DD HH:MM:SS, read as UTC. Send no zone suffix — a trailing UTC is refused with 406
end_timestring (datetime)NoNowEnd of time range, same format as start_time
workspace_idstringNo—Filter by workspace (19-digit ID)
share_idstringNo—Filter by share (19-digit ID)

Only one of workspace_id or share_id can be specified at a time.

Usage history retention. Usage history is kept at three resolutions, decided by each reading's own timestamp: a reading less than 45 days old is kept hour by hour; from 45 days to one year old it is kept as one point per UTC day; older than one year, as one point per calendar month, kept indefinitely. A total is identical at every resolution when the range is aligned to the resolution kept for that age: whole UTC days for readings 45 days to a year old, whole calendar months for readings older than a year. Ask for whole days over a range that reaches past a year and the month-resolution part of it still contributes whole months, because there is no finer detail left to divide — and the same is true of a range starting mid-day more than 45 days ago, which includes that entire UTC day.

Reading times are the exception, and they stay exact: the timestamp and value of the latest reading in each kept period are preserved, so a "last value" answer is always a real reading rather than the start or end of a day or a month.

Resolution of a long range. A range that reaches into a coarser resolution is answered at that resolution, and interval_hours reports the width the points are actually at — which can be wider than the range alone implies. Calendar-month points have no fixed width, so interval_hours is negative for them and counts months per point: -1 is one month, -3 a quarter. Several months are grouped into one point when a range is long enough that one point per month would exceed the point ceiling. Every point carries its own start_time and end_time, so read a point's span from those rather than assuming a fixed width.

History before the policy. This policy took effect on 2026-09-19 and did not recreate history that had already been discarded: nothing older than 120 days existed at that point. A range reaching further back returns zeros for the part with no history rather than an error, so a flat or empty early portion of a long range means there is no detail to show, not that there was no usage. Invoices and billed totals for closed periods are unaffected — they are kept independently of this detail. Pull and store anything you need at a finer resolution than the policy keeps.

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/usage/meters/list/?meter=storage&start_time=2026-08-01+00:00:00&end_time=2026-08-31+23:59:59" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "usage": {
    "meter": "storage",
    "total": 1073741824,
    "cost": 0.50,
    "credits": 500,
    "start_time": "2026-08-01 00:00:00 UTC",
    "end_time": "2026-08-31 23:59:59 UTC",
    "interval_hours": 24,
    "workspace_id": null,
    "share_id": null,
    "data_points": [
      {
        "start_time": "2026-08-01 00:00:00 UTC",
        "end_time": "2026-08-02 00:00:00 UTC",
        "value": 536870912,
        "cost": 0.25,
        "credits": 250
      }
    ]
  }
}

Response fields

FieldTypeDescription
usage.meterstringThe meter type queried
usage.totalnumberTotal usage value for the period
usage.costnumberTotal cost in USD
usage.creditsnumber/nullTotal credits consumed (null for direct-billed meters)
usage.start_timestringStart of the queried range
usage.end_timestringEnd of the queried range
usage.interval_hoursintegerHours per data point, chosen automatically so a range returns about 30 points (one more when the range begins part-way through a point, which is taken whole). Widened to a multiple of 24 for ranges starting more than 45 days ago. A negative value means calendar months per point (-1 one month, -3 a quarter) for ranges starting more than a year ago, where no hour count describes a point; read each point's own start_time and end_time for its exact span
usage.data_pointsarrayTime-series data with value, cost, and credits per interval

Error responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406"Must be one of the valid meter types"Invalid meter type
1605 (Invalid Input)406"Only one of workspace_id or share_id can be specified."Both filters provided
1605 (Invalid Input)406"Start time must be before end time."Invalid time range
1605 (Invalid Input)406"Time range must be at least 1 day."Range too short
1654 (Internal Error)500"Failed to retrieve usage data."Internal error

List Available Plans

GET /current/org/billing/plan/list/

Auth required. Returns the paid plans available to select when activating or upgrading an organization. Only these currently-offered paid plans are returned for selection.

New organizations choose a paid plan to activate; until then the org is in an upgrade-only state.

curl example

curl -X GET "https://api.fast.io/current/org/billing/plan/list/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

Each entry in plans is the full plan definition. The example below is abbreviated: it shows one of the six entries, trims its meters, features and limits objects, and omits a few presentation fields. The real response lists every meter, feature flag and limit.

{
  "result": true,
  "results": 6,
  "defaults": {
    "pro": "starter_monthly",
    "business": "business_v3_monthly"
  },
  "plans": [
    {
      "name": "enterprise_v2_monthly",
      "title": "Enterprise",
      "desc": "For scaling teams that need room to grow",
      "category": "business",
      "pricing": {
        "price_base": 199.99,
        "coupon": null,
        "meters": {
          "credits": {
            "price_per_unit": 0.01,
            "unit": 100,
            "free_units": 3000000,
            "unit_desc": "credit",
            "meter_type": "direct",
            "aggregation_type": "last"
          },
          "users": {
            "price_per_unit": 1,
            "unit": 1,
            "unit_desc": "seats",
            "free_units": 30,
            "meter_type": "direct",
            "aggregation_type": "average"
          }
        },
        "billing_threshold": 450,
        "billed": "monthly",
        "free": false,
        "free_days": 30,
        "free_cooldown": 5184000,
        "discount": null
      },
      "legacy_billing": false,
      "credit_overage_behavior": "metered_overage",
      "trial_credit_limit": 150000,
      "seat_limit": 200,
      "available": true,
      "show_upgrade_msg": true,
      "features": {
        "sso": true,
        "org_controls": true,
        "content_ai": true,
        "ai_agent": true
      },
      "limits": {
        "event_retention_days": 365,
        "workspaces": { "limit": 200, "members": 30 }
      }
    }
  ]
}

Response fields

FieldTypeDescription
resultsintegerNumber of available plans (each monthly and annual interval is its own entry)
defaultsobjectDefault plan identifiers per category (pro, business)
plansarrayArray of plan detail objects
plans[].namestringPlan identifier — pass this as billing_plan when subscribing (e.g., enterprise_v2_monthly)
plans[].titlestringDisplay name ("Starter", "Business", "Enterprise")
plans[].descstringShort marketing description
plans[].categorystringPlan category: "pro" (Starter), "business" (Business or Enterprise)
plans[].pricing.price_basenumberFlat plan fee in US dollars for one billing period (e.g., 199.99 = $199.99; an annual plan's value is the yearly fee)
plans[].pricing.billedstringBilling interval: "monthly" or "annual"
plans[].pricing.free_daysintegerTrial length in days; 0 means payment is due at checkout
plans[].pricing.billing_thresholdnumberUsage amount in US dollars at which an interim invoice is issued
plans[].pricing.metersobjectUsage meters keyed by meter name. direct meters bill in dollars (price_per_unit per unit, after free_units included); credits meters consume credits (credits_per_unit per unit)
plans[].credit_overage_behaviorstringWhat happens when included credits run out (e.g., "metered_overage")
plans[].trial_credit_limitintegerCredit allowance during a trial
plans[].seat_limitintegerMaximum billable seats
plans[].featuresobjectPlan feature flags (booleans), e.g. sso, org_controls, content_ai, ai_agent
plans[].limitsobjectPlan limits (storage, workspaces, shares, uploads, metadata, cloud import, and so on)

List Invoices

GET /current/org/{org_id}/billing/invoices/

Auth required. Admin or above. Returns a paginated list of invoices with hosted payment links.

Query parameters

NameTypeDefaultDescription
limitinteger10Number of invoices to return (1-100)
starting_afterstring—Invoice ID cursor for pagination

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/invoices/?limit=10" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "invoices": [
    {
      "id": "in_1234567890",
      "status": "paid",
      "currency": "usd",
      "amount_due": 2900,
      "amount_paid": 2900,
      "subtotal": 2900,
      "total": 2900,
      "paid": true,
      "description": "Subscription creation",
      "hosted_invoice_url": "https://{payment_provider_host}/i/.../{invoice_id}",
      "invoice_pdf": "https://{payment_provider_host}/invoice/.../{invoice_id}.pdf",
      "period_start": "2026-03-01 00:00:00 UTC",
      "period_end": "2026-04-01 00:00:00 UTC",
      "created": "2026-03-01 00:00:00 UTC"
    }
  ],
  "has_more": false
}

Response fields

FieldTypeDescription
invoicesarrayArray of invoice objects
invoices[].idstringInvoice identifier (use as starting_after cursor)
invoices[].statusstring"draft", "open", "paid", "void", "uncollectible"
invoices[].currencystringThree-letter ISO currency code (e.g., "usd")
invoices[].amount_dueintegerAmount due in cents
invoices[].amount_paidintegerAmount paid in cents
invoices[].subtotalintegerSubtotal before tax in cents
invoices[].totalintegerTotal after tax in cents
invoices[].paidbooleanWhether the invoice has been paid
invoices[].descriptionstring/nullInvoice description
invoices[].hosted_invoice_urlstring/nullURL to view and pay the invoice
invoices[].invoice_pdfstring/nullDirect PDF download URL
invoices[].period_startstring/nullBilling period start (YYYY-MM-DD HH:MM:SS UTC)
invoices[].period_endstring/nullBilling period end (YYYY-MM-DD HH:MM:SS UTC)
invoices[].createdstring/nullInvoice creation timestamp (YYYY-MM-DD HH:MM:SS UTC)
has_morebooleanWhether more invoices are available for pagination

Amounts are in the smallest currency unit (cents for USD). Use hosted_invoice_url to link users to their invoices. Use starting_after with the last invoice id for pagination.


Create Workspace (from Org)

POST /current/org/{org_id}/create/workspace/

Auth required. Member or above. Creates a workspace within the org. Subject to plan feature availability, workspace creation limits, and the org's workspace-create policy (see Organization Security Controls). Read capabilities.can_create_workspace on the org's details response to know in advance whether the calling user may create one.

Request parameters

NameTypeRequiredDescription
folder_namestringYesURL-safe folder name for the workspace. Must be globally unique across all workspaces.
namestringYesDisplay name.
descriptionstringNoWorkspace description.
perm_joinstringYesWho can auto-join from the org. Values: 'Member or above', 'Admin or above', 'Only Org Owners', 'No one can join automatically' (direct members only).
perm_member_managestringYesWho can manage workspace members. Values: 'Member or above', 'Admin or above'.
intelligencestringNoEnable AI features ("true"/"false"). Defaults to "true" when omitted. Forced off on plans lacking content_ai + ai_agent, which never fails the create.
metadata_extractionstringNoAutomatic metadata extraction for new uploads ("true"/"false"). Defaults to "true" when omitted. Effective only while intelligence is on and the plan includes metadata.
accent_colorstring (JSON)NoAccent color as JSON.
background_color1string (JSON)NoPrimary background color as JSON.
background_color2string (JSON)NoSecondary background color as JSON.

curl example

curl -X POST "https://api.fast.io/current/org/1234567890123456789/create/workspace/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "folder_name=project-alpha" \
  -d "name=Project Alpha" \
  -d "perm_join=Member or above" \
  -d "perm_member_manage=Admin or above"

Response (200 OK)

{
  "result": true,
  "workspace": {
    "id": "1234567890123456780",
    "folder_name": "project-alpha",
    "intelligence": true
  }
}

Response fields

FieldTypeDescription
workspace.idstring19-digit numeric workspace ID
workspace.folder_namestringURL-safe folder name
workspace.intelligencebooleanIntelligence as actually applied: false when you sent intelligence=false, or when the plan lacks content_ai or ai_agent (forced off whatever you sent)

Error responses

Error CodeHTTP StatusMessageCause
1685 (Feature Limit)412"Workspace creation is not available on your current plan."Feature disabled
1700 (Forbidden)403"Your organization does not permit you to create workspaces."The caller is below the org's perm_workspace_create threshold and is not on its allowlist. params.reason = policy_workspace_create_denied.
1685 (Feature Limit)412"You have reached your workspace creation limit."Limit exceeded
1658 (Not Acceptable)406"The supplied workspace folder name is already in use."Duplicate folder name
1605 (Invalid Input)406"An invalid workspace folder name was supplied."Invalid folder name
1605 (Invalid Input)406"An invalid configuration was supplied..."Metadata validation failed

List Workspaces in Org

GET /current/org/{org_id}/list/workspaces/

Auth required. Lists accessible workspaces within the org.

Query parameters

NameTypeDefaultDescription
archivedstring"false""true" to show archived workspaces, "false" for active
limitinteger1001-500, number of items to return
offsetinteger0Number of items to skip

Access levels

RoleAccessNotes
OwnerFull accessSees all workspaces
AdminFull accessSees all workspaces except those restricted to perm_join = 'Only Org Owners' (unless directly a member)
MemberFilteredSees workspaces matching join permission level
ExternalFilteredSees only workspaces where they are a direct member

curl example

curl -X GET "https://api.fast.io/current/org/1234567890123456789/list/workspaces/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "workspaces": [
    {
      "id": "1234567890123456780",
      "folder_name": "project-alpha",
      "name": "Project Alpha",
      "description": "Main project workspace"
    }
  ],
  "pagination": {"total": 1, "limit": 100, "offset": 0, "has_more": false}
}

Response fields

FieldTypeDescription
workspacesarrayArray of workspace objects
workspaces[].idstring19-digit numeric workspace ID
workspaces[].folder_namestringURL-safe folder name
workspaces[].namestringDisplay name
workspaces[].descriptionstring/nullWorkspace description
↑ Back to top