Organization Management Org CRUD, members, billing, discovery
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.
- Internal orgs (
member: true) — orgs you created or were invited to join as a member. You have org-level access: see all workspaces (subject to permissions), manage settings if admin, appear in the member list. Listed viaGET /current/orgs/list/. - External orgs (
member: false) — orgs you access only through workspace membership. A human invited you to their workspace but not to the org itself. You can see the org's name and basic public info, but cannot manage org settings, see other workspaces, or add org members. Listed viaGET /current/orgs/list/external/.
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
| Field | Type | Min | Max | Regex / Rules | Default |
|---|---|---|---|---|---|
| domain | string | 2 | 63 | ^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$ Lowercase alphanumeric + hyphens. Must be unique. Must not be reserved. | Required |
| name | string | 3 | 100 | Free text display name | null |
| description | string | 10 | 1000 | Free text | null |
| industry | string | — | — | Must be one of the values from GET /current/orgs/industries/ | null |
| perm_member_manage | string | — | — | 'Member or above', 'Admin or above', 'Owner only' | 'Member or above' |
| perm_workspace_create | string | — | — | 'Member or above', 'Admin or above', 'Only Org Owners'. Enterprise plan only. | 'Member or above' |
| workspace_create_allowlist | string (JSON list of user IDs, sent in this one field) | — | 500 | User IDs allowed to create workspaces regardless of perm_workspace_create. Every ID must be a current member of the org. Enterprise plan only. Returned as an array. | [] |
| sharing_shares | boolean | — | — | Whether members may create shares (Send / Receive / Exchange, including shared folders). Enterprise plan only. | true |
| sharing_file_links | boolean | — | — | Whether members may create single-file share links. Enterprise plan only. | true |
| perm_authorized_domains | string | — | — | Email domain for auto-join (e.g., acme.com) | null |
| billing_email | string (email) | — | — | Valid email with reachable domain | User's email |
| accent_color | string (JSON) | — | — | JSON-encoded color object | null |
| background_color | string (JSON) | — | — | JSON-encoded color object | null |
| background_mode | string | — | — | One of the supported background display modes | null |
Member Roles and Permissions
| Role | Level | Can manage members | Can manage settings | Can manage billing | Can close org | Can transfer ownership |
|---|---|---|---|---|---|---|
| Owner | Highest | Yes | Yes | Yes | Yes | Yes |
| Admin | High | Yes (if perm_member_manage allows) | Yes | Yes | No | No |
| Member | Standard | If perm_member_manage = 'Member or above' | No | No | No | No |
| View | Lowest | No | No | No | No | No |
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/.
| Setting | Effect |
|---|---|
perm_workspace_create | Minimum role required to create a workspace in the org. |
workspace_create_allowlist | Named users who may create a workspace regardless of the role threshold. The two are combined with OR. |
sharing_shares | When false, members may not create new shares (Send / Receive / Exchange, including shared folders). |
sharing_file_links | When 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.
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:
| Setting | Governs |
|---|---|
external_invites_shares | Shared Folders, and File Share grants. |
external_invites_portals | Portals. |
external_invites_workspaces | Workspaces. |
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" } }
adminandmemberare each"allowed"or"denied", applying to owners/admins and to ordinary members respectively (an owner reads theadminbaseline).overridesmaps a user ID to"allowed"or"denied", naming an exception to that user's role baseline. Every key must be a current member of the org — a workspace or share participant who is not an org member always reads thememberbaseline instead and can never be named here. Maximum 100 overrides per policy.- Unconfigured is permissive. An org that has never set one of these three keys behaves exactly as it did before the policy existed — nobody is restricted.
- An unreadable stored value is the one exception to "permissive is the default." It resolves to
"denied"for every caller until an admin resaves it, and it is echoed back as the raw stored string (not an object), so a client can detect and repair it rather than silently showing it as unset.
Reading and writing.
- Write:
POST /current/org/{org_id}/update/— send the whole envelope as a JSON string in the field named after the setting (see Update Organization below). Sending""or"null"clears the policy back to unconfigured; the server also accepts a literalnull. An omitted field leaves the stored value unchanged. Every write replaces the whole value — sendingoverridesas{}clears every existing exception while leaving both baselines untouched; there is no partial merge of overrides. - Read:
GET /current/org/{org_id}/details/echoes all three envelopes raw, admin-only — the stored setting the shared policy editor round-trips. - Effective answer:
capabilities.external_invites_shares/_portals/_workspaceson the same response report the calling user's own resolved answer (their role, or their override) as booleans. These are member-visible, and a client should read them to decide whether to show an invite control rather than recomputing the effective answer from the raw envelope.
Enforcement. The org policy is the first half of the answer; each Portal, Shared Folder, Workspace and File Share also carries its own external_invites flag (allowed / denied, absent = inherit the org policy — see the Shares and Workspaces references). 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:
| Reason | Meaning |
|---|---|
external_invites_denied | The org's policy denies this inviter. |
external_invites_object_denied | The object's own external_invites flag denies it — the remedy is that object's admin, not the org admin. |
plan_required | A 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 widening an object's own public-access setting, including 'Registered users with an 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:
| Family | Governs |
|---|---|
api_keys | Every API key issued by a member of this org. |
oauth | Every 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": {}
}
}
- Each role's value is
{"max_mode": "r"|"rw"|"rwa", "scope_types": [...]}. max_modeis a ceiling: the highest access mode a credential in that family may hold for any one entity governed by this org. It reads the opposite way from an ordinary scope check — a credential is refused here for holding too much, not too little.scope_typesis the subset of entity types a credential in that family may hold a grant for against this org —org,workspace,share,fileshare. (The retiredworkflowandsign_envelopeentity types are not offered; they are not selectable here.) Account-only authority (user:*,memory:*,userdetails:*) is never refused byscope_types— an account-wide key is used against every org its holder can reach, so refusing it by type here would disable it everywhere else — but it is still bounded bymax_mode, evaluated locally against whichever org a given request actually touches.overridesmaps a user ID to its own{max_mode, scope_types}value, naming an exception to that user's role baseline. Every key must be a current member of the org. Maximum 100 overrides per family.- Unconfigured is permissive — an org that has never set
credential_policy, or has cleared a family, behaves exactly as it did before the policy existed: every mode, every attributable type. - An unreadable stored value is the one exception to "permissive is the default." It resolves to the restrictive pole — every credential in that family refused — until an admin resaves it, and it is echoed back as the raw stored string so a client can detect and repair it.
Reading and writing.
- Write:
POST /current/org/{org_id}/update/— send the wholecredential_policyobject as a JSON string in thecredential_policyfield (see Update Organization below). An omitted family is left unchanged; a family set tonullis cleared back to unconfigured. Enterprise plan only to tighten — a write that lowers amax_modeor removes ascope_typesentry, for either family, for anyone (admin,member, or an override), is refused with HTTP 403 andparams.reason=plan_requiredon an org without the plan. - Read:
GET /current/org/{org_id}/details/echoescredential_policyraw, admin-only, pluscapabilities.credential_policy_api_keysandcapabilities.credential_policy_oauth— the calling user's own effective{max_mode, scope_types}for each family, so a key-creation form can pre-filter its scope and mode pickers without recomputing the resolution itself.
Enforcement.
- At issuance — creating or updating an API key (see API Keys in the Auth reference), and at OAuth consent (see the OAuth 2.0 reference), the requested (or, on an update, the effective) scopes are checked against the policy of each grant's own owning org.
- At every later request — unlike the org security controls above, which are read-time or create-time only,
credential_policyis re-checked on every authenticated request an API key or OAuth token makes that resolves to a governing org. Tightening the policy can make an already-issued, already-working credential start failing on its very next call, with no revocation event and no grace period.
Refusals are 403 with params.reason:
| Reason | Meaning |
|---|---|
credential_policy_mode | The 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_scope | The credential names an entity type this family's scope_types does not allow. |
credential_policy_sso | The credential owner's account is on this org's SSO-enforcing domain and is not exempt — see Credential-request enforcement in the SSO reference. |
- A signed-in browser session is never subject to
credential_policy— it carries noscopesclaim, exactly as it is exempt from the ordinary scope checks in the Auth reference. - A public File Share single-file link carries no API key and no account session; it is not an org-governed credential and is never checked, at issuance or at request time.
- Three collection endpoints that list an account's own orgs and shares in bulk (
orgs/list,orgs/all,shares/all) resolve no single governing org per row and are permanently outside the request-time check — a stated limit, not an oversight. - A lookup failure — the policy could not be read, or the owning org could not be resolved — is
503(temporarily unavailable), never a silent pass and never a401. - Only the authority actually used for a request is checked — the concrete or wildcard grant that satisfied that request's entity, never the credential's whole scope set and never an inherited parent grant. A key holding
org:A:rwaandorg:B:ris checked against org A's policy only while it is acting in org A.
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" }
enabled(bool) — whether cloud sync runs at all for the caller this value resolves to.falsestops sync in both directions; a source parks atstatus: "suspended_policy"and resumes on its own once the policy re-enables it.mode("read"or"read_write") — whether local edits are pushed back to the provider."read"stops only the outbound half; inbound sync continues. Amode-only flip changes no source status.- Unconfigured is permissive —
{enabled: true, mode: "read_write"}. An unreadable stored value is the one exception: it resolves to the restrictive pole (enabled: false) for every caller until an admin resaves it, echoed back raw so a client can detect and repair it. - This is the org half only. The effective answer for a workspace is the org value resolved first, then met with that workspace's own
cloud_sync_modeceiling — see Cloud Sync Policy in the Workspaces reference for the full resolution order, the per-sourceeffective_access_modefields, and how a queued write-back behaves under areadpolicy (deferred, not failed, with a bounded ~5-day hold).
Reading and writing.
- Write:
POST /current/org/{org_id}/update/— send the whole envelope as a JSON string in thecloud_syncfield (see Update Organization below). Sending""/"null"/nullclears the policy back to unconfigured. An omitted field leaves the stored value unchanged. - Read:
GET /current/org/{org_id}/details/echoescloud_syncraw, admin-only. There is no org-level effective-answer field: the effective answer is the workspace'seffective_cloud_sync, because cloud sync is a per-workspace feature and the org value alone cannot say whether a given workspace's own setting narrows it further.
Refusals are 403 with params.reason:
| Reason | Raised at |
|---|---|
cloud_sync_disabled | Source create, each provider's OAuth-complete endpoint, and any write-back action, while enabled is false somewhere in the org-then-workspace chain. |
cloud_sync_read_only | A 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.
Audit. org_updated carries cloud_sync inside its existing policy_changes map: policy_changes.cloud_sync = { before: {enabled, mode}, after: {enabled, mode}, overrides: {added, removed, changed} }.
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" } }
adminandmemberare each"required"or"optional", applying to owners/admins and to ordinary members respectively (an owner reads theadminbaseline).overridesmaps a user ID to"required"or"optional", naming an exception to that user's role baseline. Every key must be a current member of the org. Maximum 100 overrides.- Unconfigured is permissive — an org that has never set this key behaves exactly as it did before the policy existed:
optionalfor everyone. - An unreadable stored value is the one exception to “permissive is the default.” It resolves to the restrictive pole,
"required", for every caller until an admin resaves it, and it is echoed back as the raw stored string so a client can detect and repair it.
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.
- Write:
POST /current/org/{org_id}/update/— send the whole envelope as a JSON string in theauth_require_2fafield (see Update Organization below). Sending""or"null"clears the policy back to unconfigured; the server also accepts a literalnull. An omitted field leaves the stored value unchanged. Enterprise plan only to tighten — a write movingoptional/unconfigured torequired, foradmin,member, or an override. - Read:
GET /current/org/{org_id}/details/echoesauth_require_2faraw, admin-only — the stored setting the shared policy editor round-trips. There is nocapabilitiestwin. Every other envelope policy on this page also reports the calling user's own resolved answer undercapabilities; this one does not, because the effective answer is about a person signing in, not a place being viewed, and it is already delivered where it is actionable —enrol_requiredon the login response — rather than by walking every org the reader belongs to on an ordinary org read.
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:
- "This policy could not be checked against your own account. Please try again." — the self-lockout test could not be evaluated, so the endpoint established neither a refusal nor a pass and declined to guess.
- "This policy change could not be scheduled. Please try again." — the write tightened the policy, but the session revocation that tightening requires could not be scheduled. The write is then deliberately abandoned: the policy is NOT stored. A tightening that committed with no revocation behind it would leave every newly-required user holding the session they already had, with nothing scheduled to end it and nothing saying so — so the endpoint refuses rather than storing half the change. Do not treat this as “probably applied, refresh later”; re-send the same request.
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:
- A user who is already enrolled is untouched — enrollment already satisfies the new requirement, so revoking them would cost a re-login with no purchase. This also means a session minted before its holder enrolled survives the flip; there is no claim on the token itself that distinguishes it.
- A user who becomes
requiredby joining the org, or by being promoted into a role whose baseline requires it, is not swept — neither is a policy write, so there is nothing for the sweep to hang off; they are challenged at their next login instead.
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.
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.
| Level | Fields returned on each org (cumulative) |
|---|---|
terse | id, domain, name, logo |
standard | terse + 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) |
full | standard + 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.
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
/current/org/create/
Auth required. Creates a new organization. The authenticated user becomes the owner.
Request parameters
| Name | Type | Required | Description |
|---|---|---|---|
| domain | string | Yes | 2-63 chars, lowercase alphanumeric + hyphens, must be unique and not reserved. Used as the org identifier in URLs. |
| name | string | No | 3-100 chars. Display name for the org. |
| description | string | No | Organization description. |
| industry | string | No | Industry type from predefined list (see GET /current/orgs/industries/). |
| accent_color | string (JSON) | No | Brand accent color as JSON. |
| background_color | string (JSON) | No | Background color as JSON. |
| background_mode | string | No | Background display mode. |
| facebook_url | string (URL) | No | Facebook page URL. Must be valid URL. |
| twitter_url | string (URL) | No | Twitter profile URL. Must be valid URL. |
| instagram_url | string (URL) | No | Instagram profile URL. Must be valid URL. |
| youtube_url | string (URL) | No | YouTube channel URL. Must be valid URL. |
| homepage_url | string (URL) | No | Organization website URL. Must be valid URL. |
| perm_member_manage | string | No | Who can manage members. See Org Field Constraints above. |
| perm_authorized_domains | string | No | Authorized email domain for auto-join. |
| billing_email | string (email) | No | Billing contact email. Defaults to user's email. |
A newly created organization must select a paid plan (Starter, Business, Growth, 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 other than Enterprise begin a 14-day free trial. Enterprise and all annual plans have no trial — Enterprise is billed at signup, and an annual subscription is billed for the full term at signup. A free 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 free trial is offered on a later org, no matter how much time has passed. A secondary per-user cooldown (60 days on modern plans, anchored at trial start) also applies on top of this, though in practice it only matters on the first org itself. 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 14: 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) — free 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 free trial is offered — the org still subscribes but bills immediately. This is the primary block and is permanent, so there is no trial_available_at:
{
"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 free trial can also be blocked by the secondary per-user cooldown (60 days on modern plans, anchored at trial start) — in practice this only matters on the first org itself. That case does include trial_available_at:
{
"result": true,
"org": { "...": "..." },
"has_free_trial": false,
"requires_payment": true,
"is_agent": false,
"no_trial_reason": "A free trial was started recently. A new free trial is available in 43 days.",
"trial_available_at": "2026-08-05 12:00:00 UTC"
}
requires_payment is always true for new orgs (the legacy free tier is closed). 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
| Field | Type | Description |
|---|---|---|
| org | object | Organization resource object |
| org.id | string | 19-digit numeric organization ID |
| org.domain | string | URL-safe subdomain |
| org.name | string/null | Display name |
| org.description | string/null | Description |
| org.logo | string/null | Logo asset URL |
| org.accent_color | string/null | Brand color |
| org.closed | boolean | Whether org is closed |
| org.suspended | boolean | Whether org is suspended |
| has_free_trial | boolean | Whether a free trial is available at checkout. true only when this is the owner's first organization and the per-user cooldown has also cleared; false for any org after the first (permanent), or when within the per-user cooldown. |
| requires_payment | boolean | Whether a paid plan is required before the org can be used. true for new orgs. |
| is_agent | boolean | Whether the creating user is an agent account |
| no_trial_reason | string | Human-readable reason a free trial is unavailable (only when has_free_trial is false) |
| trial_available_at | string | UTC timestamp (Y-m-d H:i:s UTC) when the user's next free trial becomes available. Present only when blocked by the per-user cooldown — absent when blocked by the first-organization rule, since that block is permanent and has no future date. |
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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
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 |
1680 (Access Denied) | 401 | GEO/risk restriction message | Request blocked by geo/risk check |
Get Org Details
/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
| Role | Access | Notes |
|---|---|---|
| Owner | Full access | Full access to all organization settings and security configuration |
| Admin | Extended access | Includes billing info, permissions, subscriber status, credit balance |
| Member | Standard access | Basic org info, plan, subscriber status (boolean only — no credit balance) |
| View | Limited access | Public 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": "#0066CC",
"closed": false,
"locked": false,
"suspended": false,
"created": "2024-01-15 10:30:00 UTC",
"updated": "2024-06-20 14:45:00 UTC"
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| org.id | string | 19-digit numeric organization ID |
| org.domain | string | URL-safe subdomain |
| org.name | string/null | Display name |
| org.description | string/null | Description |
| org.logo | string/null | Logo asset URL |
| org.accent_color | string/null | Brand color |
| org.closed | boolean | Whether org is closed |
| org.locked | boolean | Whether org is locked |
| org.suspended | boolean | Whether org is suspended |
| org.created | string | Creation timestamp |
| org.updated | string | Last update timestamp |
| org.plan | string | Billing plan identifier (e.g., "solo_monthly", "business_v2_monthly", "growth_monthly"). Member+ only. |
| org.subscriber | boolean | Whether the org has an active subscription (includes credit availability for free-tier orgs). Member+ only. |
| org.subscriber_trial_until | integer/null | Unix timestamp when the trial period ends. null for paid plans or if no trial. Member+ only. |
| org.payment_state | string | Payment 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_at | string/null | Start 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 — an immediate upgrade charge, for instance — the period start can be materially earlier than the failure. 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_at | null | Reserved. Always null: the date access ends is not currently knowable, so none is reported rather than an estimate. Member+ only. |
| org.subscriber_trial_credits | integer/null | Credits remaining in the current billing period. Admin+ only. |
| org.perm_workspace_create | string | Minimum role required to create a workspace. Admin+ only. |
| org.workspace_create_allowlist | array of string | User IDs allowed to create a workspace regardless of the threshold. Admin+ only. |
| org.sharing_shares | boolean | Whether members may create shares. Admin+ only. |
| org.sharing_file_links | boolean | Whether members may create single-file share links. Admin+ only. |
| org.external_invites_shares | object/string/null | Raw 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_portals | object/string/null | Same shape, governing Portal invitations. Admin+ only. |
| org.external_invites_workspaces | object/string/null | Same shape, governing Workspace invitations. Admin+ only. |
| org.credential_policy | object/string/null | Raw 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.auth_require_2fa | object/string/null | Raw 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.capabilities.can_create_workspace | boolean | Whether the calling user may create a workspace in this org right now — role, plan and policy combined. Member+ only, details responses only. |
| org.capabilities.sso | boolean | Whether the org's plan includes identity-provider configuration. Member+ only, details responses only. |
| org.capabilities.org_controls | boolean | Whether the org's plan includes the security controls above. Member+ only, details responses only. |
| org.capabilities.external_invites_shares | boolean | Whether 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_portals | boolean | Same, for Portals. Member+ only, details responses only. |
| org.capabilities.external_invites_workspaces | boolean | Same, for Workspaces. Member+ only, details responses only. |
| org.capabilities.credential_policy_api_keys | object | The calling user's own effective {max_mode, scope_types} for API keys issued in this org, combining role and override. Visible to org members and to a workspace or share participant who is not an org member (reading the member baseline), details responses only. |
| org.capabilities.credential_policy_oauth | object | Same, for OAuth grants. Visible to org members and to a workspace or share participant who is not an org member (reading the member baseline), details responses only. |
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1680 (Access Denied) | 401 | "You have not been granted access to this Org." | Insufficient permission |
1688 (Subscription Required) | 402 | "The organization does not have an active subscription." | Org has no active subscription or free-tier credits exhausted |
1696 (Credit Limit Exceeded) | 402 | "You have exceeded your credit limit." | Free-tier credit limit exceeded |
Get Public Org Details
/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": "#0066CC"
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| org.id | string | 19-digit numeric organization ID |
| org.domain | string | URL-safe subdomain |
| org.name | string/null | Display name |
| org.description | string/null | Description |
| org.logo | string/null | Logo asset URL |
| org.accent_color | string/null | Brand color |
Update Organization
/current/org/{org_id}/update/
Auth required. Admin or above. Updates org details. Only provided fields are modified.
Access levels
| Role | Access |
|---|---|
| Owner | Full access |
| Admin | Full access |
| Member | Denied |
Request parameters (all optional)
| Name | Type | Description |
|---|---|---|
| domain | string | New URL-safe subdomain (2-63 chars, lowercase alphanumeric + hyphens). |
| name | string | Display name (3-100 chars). Send "null" to clear. |
| description | string | Description. Send "null" or "" to clear. |
| industry | string | Industry type from predefined list. |
| accent_color | string (JSON) | Brand accent color as JSON. Send "null" to clear. |
| background_color | string (JSON) | Background color as JSON. Send "null" to clear. |
| background_mode | string | Background display mode. |
| use_background | string | Enable/disable background ("true"/"false"). |
| facebook_url | string (URL) | Facebook URL. |
| twitter_url | string (URL) | Twitter URL. |
| instagram_url | string (URL) | Instagram URL. |
| youtube_url | string (URL) | YouTube URL. |
| homepage_url | string (URL) | Organization website URL. |
| perm_member_manage | string | Member management permission level. |
| perm_workspace_create | string | Minimum role required to create a workspace. Enterprise plan only. |
| workspace_create_allowlist | string (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. |
| sharing_shares | string | Whether members may create shares. Send the string "true" or "false", form-encoded or as a query parameter. Enterprise plan only. |
| sharing_file_links | string | Whether members may create single-file share links. Send the string "true" or "false", form-encoded or as a query parameter. Enterprise plan only. |
| external_invites_shares | string (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_portals | string (JSON) | Same shape, governing Portal invitations. |
| external_invites_workspaces | string (JSON) | Same shape, governing Workspace invitations. |
| credential_policy | string (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. |
| auth_require_2fa | string (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. |
| perm_authorized_domains | string | Authorized email domain for auto-join. |
| billing_email | string (email) | Billing contact email. Domain must be reachable. |
| owner_defined | string (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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
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 | "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 |
1700 (Forbidden) | 403 | "This configuration requires an Enterprise plan." | A security-control write that tightens a setting was sent by an org without the entitlement. Resubmits and relaxations are not refused. params.reason = plan_required. |
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. |
1663 (Update Failed) | 500 | "There was an internal error processing your update request." | Internal error |
Close Organization
/current/org/{org_id}/close/
Auth required. Owner only. Soft-deletes the organization. Active subscriptions are automatically cancelled.
Request parameters
| Name | Type | Required | Description |
|---|---|---|---|
| confirm | string | Yes | Must 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"
This response shape is the same whether or not a free trial applies. Confirm the 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.
- Trial applies — the subscription starts in
trialingand the card is charged when the trial ends. - No trial (all annual plans, and any org that has subscribed before) — the subscription is created and charged immediately once the card is captured.
The card is inspected before any charge: a card refused by the funding rules does not produce a subscription, and the customer's billing address is taken from the card, so the first invoice is taxed correctly.
Response (202 Accepted)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
120445 | 406 | "The confirm field is required. Pass the org's domain or numeric id as confirm." | confirm was not provided |
10549 | 406 | "The confirm field provided does not match the org's domain or id." | Confirmation does not match domain or ID |
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
/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,
"assets": [
{
"name": "logo",
"mime_types": ["image/png", "image/jpeg"],
"max_size": 5242880
}
]
}
List Org Assets
/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": {
"url": "https://assets.fast.io/org/logo.png",
"mime_type": "image/png",
"size": 45678
}
}
}
Upload Org Asset
/current/org/{org_id}/assets/{asset_name}/
Auth required. Admin or above. Upload as multipart/form-data.
Request parameters
| Name | Type | Required | Description |
|---|---|---|---|
| file | file (multipart) | Yes | The asset file to upload. |
| metadata | string (JSON array) | No | Additional metadata for the asset. |
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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
1691 (File Missing) | 412 | "Asset upload missing" | No file in the request |
1605 (Invalid Input) | 406 | "metadata invalid" | Metadata is not a valid array |
Delete Org Asset
/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)
/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
/current/org/{org_id}/members/{email_or_user_id}/
Auth required. Permission governed by org's perm_member_manage setting.
The target is specified as a path parameter:
- Use a user ID (19-digit numeric) to add an existing user directly
- Use an email address to send an invitation
Request parameters (adding existing user by ID)
| Name | Type | Required | Description |
|---|---|---|---|
| permissions | string | Yes | Permission level: "member", "admin". Cannot add as "owner". |
| expires | string (datetime) | No | Membership expiration date. |
| notify | string | No | Notification preference. |
| force_notification | boolean | No | Force send a notification email. |
Request parameters (inviting by email)
| Name | Type | Required | Description |
|---|---|---|---|
| permissions | string | Yes | Permission level for the invitation: "member", "admin". |
| message | string | No | Custom invitation message. |
| expires | string (datetime) | No | Invitation expiration. |
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": "eA1B2C3D4E5F6G7H8J9K0L1M2N3O4",
"invitee_email": "jane@example.com",
"entity_type": "org",
"state": "pending",
"created": "2024-01-15 10:30:00 UTC"
}
}
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "Invalid permission specified." | Invalid permission value |
1692 (Cannot Add As Owner) | 500 | "Adding a member as an owner is not allowed" | Tried to add as owner (use transfer_ownership) |
1656 (Limit Exceeded) | 413 | Limit message | Member limit exceeded |
Remove a Member
/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
/current/org/{org_id}/members/list/
Auth required. Any org member. Paginated.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| limit | integer | 100 | 1-500, number of items to return |
| offset | integer | 0 | Number 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"
},
{
"id": "1234567890123456780",
"account_type": "agent",
"email_address": "bot@example.com",
"first_name": "Service",
"last_name": "Bot",
"permissions": "admin"
}
],
"pagination": {
"total": 2,
"limit": 50,
"offset": 0,
"has_more": false
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| users | array | Array of member objects |
| users[].id | string | 19-digit numeric user ID |
| users[].account_type | string | "human" or "agent" |
| users[].email_address | string | User's email |
| users[].first_name | string | First name |
| users[].last_name | string | Last name |
| users[].permissions | string | Role: "owner", "admin", "member" |
| pagination.total | integer | Total number of members |
| pagination.limit | integer | Requested page size |
| pagination.offset | integer | Current offset |
| pagination.has_more | boolean | Whether more results exist |
Leave Organization (Self)
/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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
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
/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,
"member": {
"id": "9876543210987654321",
"account_type": "human",
"email_address": "jane@example.com",
"first_name": "Jane",
"last_name": "Smith",
"permissions": "admin",
"invite": "accepted",
"notify": "Email me",
"expires": null,
"member_added_at": "2026-08-29 15:48:29 UTC"
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| member.id | string | 19-digit numeric user ID |
| member.account_type | string | "human" or "agent" |
| member.email_address | string | User's email |
| member.first_name | string | First name |
| member.last_name | string | Last name |
| member.permissions | string | Role: "owner", "admin", "member" |
| member.invite | string | Invitation status |
| member.notify | string | Notification preference |
| member.expires | string/null | Membership expiration (YYYY-MM-DD HH:MM:SS UTC) or null for no expiry |
| member.member_added_at | string | When 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 |
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "The membership you specified does not exist." | User is not a member |
Update Member Permissions
/current/org/{org_id}/member/{user_id}/update/
Auth required. Permission governed by org's perm_member_manage setting.
Request parameters (all optional)
| Name | Type | Description |
|---|---|---|
| permissions | string | New permission level ("member", "admin") |
| expires | string (datetime) | Membership expiration date |
| notify | string | Notification preference |
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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "The membership you specified does not exist." | User is not a member |
Transfer Org Ownership
/current/org/{org_id}/member/{user_id}/transfer_ownership/
Auth required. Owner only. Transfers ownership of the org to the specified member. The current owner is demoted to admin.
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
}
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "You cannot transfer ownership to yourself." | Target is the current user |
1605 (Invalid Input) | 406 | "The membership you specified does not exist." | User is not an org member |
1605 (Invalid Input) | 406 | "Member is already an owner." | Target is already the owner |
1663 (Update Failed) | 500 | "Failed to update owner of the org." | Internal error |
Join Organization
/current/org/{org_id}/members/join/
Auth required. Join an org via invite or domain-based auto-join.
Join methods
- Via invitation: Append the invitation key to the URL path:
.../join/{invitation_key}/optionally followed byacceptordecline. Default isaccept. - Via authorized domain: The org must have
perm_authorized_domainsset and the user's email domain must match. User is added as a Member.
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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
1680 (Access Denied) | 401 | "This org does not allow you to join automatically..." | Domain auto-join not enabled |
1680 (Access Denied) | 401 | "You are not allowed to join this org automatically..." | User's email domain does not match |
1656 (Limit Exceeded) | 413 | Limit message | Member limit exceeded |
List Org Invitations
/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": "eA1B2C3D4E5F6G7H8J9K0L1M2N3O4",
"inviter": "John Doe",
"invitee_email": "jane@example.com",
"entity_type": "org",
"state": "pending",
"created": "2024-01-15 10:30:00 UTC",
"expires": "2024-02-15 10:30:00 UTC"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| invitations | array | Array of invitation objects |
| invitations[].id | string | Invitation identifier |
| invitations[].inviter | string | Name of the user who sent the invitation |
| invitations[].invitee_email | string | Email address of the invitee |
| invitations[].entity_type | string | Always "org" for org invitations |
| invitations[].state | string | Invitation state: "pending", "accepted", "declined" |
| invitations[].created | string | Creation timestamp |
| invitations[].expires | string/null | Expiration timestamp |
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "An invalid invitation state was supplied." | Invalid state filter |
Update an Invitation
/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.
Request parameters (all optional)
| Name | Type | Description |
|---|---|---|
| state | string | New invitation state |
| permissions | string | Updated permission level |
| expires | string (datetime) | Updated expiration date |
curl example
curl -X POST "https://api.fast.io/current/org/1234567890123456789/members/invitation/eA1B2C3D4E5F6G7H8J9K0L1M2N3O4/" \
-H "Authorization: Bearer {jwt_token}" \
-d "permissions=admin"
Response (200 OK)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "An invalid invitation ID or email was supplied." | Invalid identifier |
1605 (Invalid Input) | 406 | "Invitation not found." | Invitation does not exist |
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 |
1679 (Update Failed) | 500 | "Failed to update invitation." | Internal error |
Delete an Invitation
/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.
curl example
curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/members/invitation/eA1B2C3D4E5F6G7H8J9K0L1M2N3O4/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1666 (Delete Failed) | 500 | "Failed to delete invitation." | Deletion failed |
Organization Discovery
List Internal Orgs
/current/orgs/list/
Auth required. Lists orgs where the user is a direct member (member: true).
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": "#0066CC",
"closed": false,
"suspended": false,
"subscriber": true,
"user_status": "joined",
"member": true
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| orgs | array | Array of organization objects |
| orgs[].id | string | 19-digit numeric organization ID |
| orgs[].domain | string | URL-safe subdomain |
| orgs[].name | string/null | Display name |
| orgs[].description | string/null | Description |
| orgs[].logo | string/null | Logo asset URL |
| orgs[].accent_color | string/null | Brand color |
| orgs[].closed | boolean | Whether org is closed |
| orgs[].suspended | boolean | Whether org is suspended |
| orgs[].subscriber | boolean | Whether org has an active subscription |
| orgs[].user_status | string | "joined" or "available" |
| orgs[].member | boolean | Always true for this endpoint |
Subscription filtering
| User Role | Behavior |
|---|---|
| Owner | Always sees the org |
| Admin | Always sees the org |
| Member | Only sees the org if it has an active subscription |
List External Orgs
/current/orgs/list/external/
Auth required. Lists orgs where the user has access only through workspace membership (member: false).
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": "#FF6600",
"closed": false,
"suspended": false,
"subscriber": true,
"user_status": "available",
"member": false
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| orgs | array | Array of external organization objects |
| orgs[].user_status | string | Always "available" for external orgs |
| orgs[].member | boolean | Always false for this endpoint |
List All Orgs
/current/orgs/all/
Auth required. Lists all accessible orgs (joined + invited).
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": "#0066CC",
"closed": false,
"suspended": false,
"user_status": "joined"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| orgs[].user_status | string | "joined" (already a member) or "available" (pending invitation) |
List Available Orgs
/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": "#FF6600",
"closed": false,
"suspended": false
}
]
}
Check Domain Availability
/current/orgs/check/domain/{domain_name}
Auth required. Checks if an org domain name is available for use.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| {domain_name} | string | Yes | The 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 Code | HTTP Status | Message | Cause |
|---|---|---|---|
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
/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,
"technology": {
"title": "Technology",
"description": "Software, hardware, and IT services"
},
"healthcare": {
"title": "Healthcare",
"description": "Medical, pharmaceutical, and health services"
},
"finance": {
"title": "Finance",
"description": "Banking, investment, and financial services"
},
"education": {
"title": "Education",
"description": "Schools, universities, and training providers"
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| {key} | string | Machine-readable industry identifier (use this value in create/update requests) |
| {key}.title | string | Human-readable display name |
| {key}.description | string | Brief 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:
| Surface | What 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_plan | The 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_plan | The 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
/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
| Name | Type | Required | Description |
|---|---|---|---|
| billing_plan | string | Yes | Target plan ID (a valid, currently-offered paid plan) |
| proration_date | integer | No | The 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. |
curl example
curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/preview/?billing_plan=growth_monthly" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"preview": {
"amount_due_cents": 20132,
"currency": "usd",
"proration_date": 1756670400,
"source_plan": "business_v2_monthly",
"target_plan": "growth_monthly",
"spend_increasing": true,
"ends_trial": false
}
}
| Field | Type | Description |
|---|---|---|
| preview.amount_due_cents | integer | Total due today, in cents, including tax. Divide by 100 before display. |
| preview.currency | string | ISO currency code |
| preview.proration_date | integer | Unix 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_plan | string | Plan the subscription currently holds |
| preview.target_plan | string | Plan being previewed |
| preview.spend_increasing | boolean | true when the change increases committed spend and will therefore invoice immediately |
| preview.ends_trial | boolean | true when confirming also ends a running free trial. The copy must say so — “this ends your trial and charges $X today” reads very differently from “this charges $X today”. |
Errors
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 | 400 | billing_plan missing or not a currently-offered plan |
10764 | 406 | No 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 free trial that they are being charged. |
Create or Update Subscription
/current/org/{org_id}/billing/
Auth required. Admin or above. Creates or updates the org's billing subscription.
Request parameters
| Name | Type | Required | Description |
|---|---|---|---|
| billing_plan | string | No | Target plan ID (must be a valid, currently-offered paid plan, e.g., "solo_monthly", "business_v2_monthly", "growth_monthly"). Each plan also has an annual variant (e.g., "business_v2_annual"). Plan IDs that are not currently offered are rejected here (existing orgs on them are unaffected). |
curl example
curl -X POST "https://api.fast.io/current/org/1234567890123456789/billing/" \
-H "Authorization: Bearer {jwt_token}" \
-d "billing_plan=business_v2_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": null,
"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.
Response (202 Accepted) — subscription updated.
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.
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
1605 (Invalid Input) | 406 | "An invalid plan was supplied." | Plan ID not recognized |
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 | "An error occurred creating the payment intent..." | Intent creation failed |
Schedule Subscription Cancellation
/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.
curl example
curl -X DELETE "https://api.fast.io/current/org/1234567890123456789/billing/" \
-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):
{
"result": true,
"status": "already_cancelled",
"message": "Subscription is already cancelled"
}
Response fields
| Field | Type | Description |
|---|---|---|
| status | string | "scheduled_cancellation" or "already_cancelled" |
| message | string | Human-readable status message |
| cancel_at | integer/null | Unix timestamp when access will end. null only if the subscription record could not be re-read after scheduling. |
| cancel_at_period_end | boolean | Always true on a successful schedule |
| closed | boolean | Always false for the scheduled-cancel flow — the org remains open until cancel_at |
Notes
- The customer retains full subscriber access (and continues to count against billing) until
cancel_at. current_period_endandcancel_atare also reflected onGET /current/org/{org_id}/billing/details/so UIs can render an "ends on YYYY-MM-DD" affordance.
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
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 |
Reactivate Subscription
/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
| Field | Type | Description |
|---|---|---|
| status | string | Always "reactivated" on success |
| message | string | Human-readable status message |
| current_period_end | integer/null | Unix timestamp of the next renewal |
| cancel_at_period_end | boolean | Always false on success |
Notes
- Calling
PUTon a subscription that is not currently scheduled to cancel is a successful no-op. - Once
cancel_athas passed and the subscription has terminated, the org is no longer a subscriber andPUTreturns 404. UsePOST /current/org/{org_id}/billing/to start a new subscription instead.
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
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
/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}"
}
}
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
| Field | Type | Description |
|---|---|---|
| billing_status.subscription | object | Payment provider subscription details |
| billing_status.customer | object | Payment provider customer details |
| billing_status.setup_intent | object/null | Active setup intent if exists |
| billing_status.setup_intent.trial | boolean | Whether the intent was minted with a free 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_intent | object/null | Active payment intent if exists |
| billing_status.payment_recovery | object | Present (non-empty) when an existing subscription's invoice is unpaid — status incomplete, past_due, or unpaid (e.g. a declined first payment). 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}. The client completes payment by confirming that PaymentIntent's client_secret with a (new) card. Empty when there is nothing to recover. |
| billing_status.active | boolean | Whether subscription is currently active |
| billing_status.free_trial_eligible | boolean | Whether a free 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 free 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 when the owner is within the per-user 60-day trial cooldown. |
| billing_status.current_plan | object | Full 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_plan | object | Full 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_key | string | Payment provider publishable key |
Get Credit Usage
/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": true,
"free_org_mode": false,
"org_id": "1234567890123456789",
"plan": "solo_monthly",
"over_limit": false,
"usage": {
"credits_used": 1200,
"credit_limit": 300000,
"credits_remaining": 298800,
"usage_percentage": 0.4
},
"period": {
"start": "2025-01-15 10:00:00 UTC",
"end": "2025-02-14 10:00:00 UTC",
"days_total": 30,
"days_elapsed": 10,
"days_remaining": 20
},
"renewal": {
"interval_days": 30,
"next_renewal": "2025-02-14 10:00:00 UTC"
},
"trial": null
}
Response fields
| Field | Type | Description |
|---|---|---|
| credit_limits_enabled | boolean | Whether the plan enforces credit limits |
| free_org_mode | boolean | true for free orgs (reductive model) |
| over_limit | boolean | Whether the org has exceeded its credit limit |
| usage.credits_used | integer | Credits consumed in the current period |
| usage.credit_limit | integer | Total credits available per period |
| usage.credits_remaining | integer | Credits remaining in the current period |
| usage.usage_percentage | number | Percentage of credits used |
| period.start | string | Start of the current billing period (YYYY-MM-DD HH:MM:SS UTC) |
| period.end | string | End of the current billing period (YYYY-MM-DD HH:MM:SS UTC) |
| period.days_total | integer | Total days in the period |
| period.days_elapsed | integer | Days elapsed since period start |
| period.days_remaining | integer | Days remaining until renewal |
| renewal.interval_days | integer | Days between credit renewals |
| renewal.next_renewal | string/null | Next credit renewal timestamp (YYYY-MM-DD HH:MM:SS UTC), or null |
| run_rate | object/null | Usage rate projections (shown after 25% of period or credits used) |
| trial | object/null | 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).
List Billable Members
/current/org/{org_id}/billing/usage/members/list/
Auth required. Admin or above. Paginated.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| limit | integer | 100 | 1-500 |
| offset | integer | 0 | Items 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": [
{
"id": "1234567890123456789",
"account_type": "human",
"email_address": "user@example.com",
"parents": {
"9876543210987654321": {
"permission": "member",
"date_joined": "2024-01-15 10:30:00 UTC"
}
}
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| billable_members | array | Array of billable member objects |
| billable_members[].id | string | 19-digit user ID |
| billable_members[].account_type | string | "human" or "agent" |
| billable_members[].email_address | string | User's email |
| billable_members[].parents | object | Map of workspace IDs to membership details |
Get Usage Meters
/current/org/{org_id}/billing/usage/meters/list/
Auth required. Admin or above. Returns detailed usage breakdown by meter.
Query parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| meter | string | Yes | — | Meter type (e.g., "storage_bytes", "bandwidth_bytes", "ai_tokens") |
| start_time | string (datetime) | No | 30 days ago | Start of time range |
| end_time | string (datetime) | No | Now | End of time range |
| workspace_id | string | No | — | Filter by workspace (19-digit ID) |
| share_id | string | No | — | Filter by share (19-digit ID) |
Only one of workspace_id or share_id can be specified at a time.
Usage history retention. Detailed usage history is kept for a bounded window; detail older than that window is removed automatically. A request whose range reaches past the window returns zeros for the part that is no longer retained rather than an error, so a flat or empty early portion of a long range means the detail has aged out, 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 to keep for longer.
curl example
curl -X GET "https://api.fast.io/current/org/1234567890123456789/billing/usage/meters/list/?meter=storage_bytes&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_bytes",
"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
| Field | Type | Description |
|---|---|---|
| usage.meter | string | The meter type queried |
| usage.total | number | Total usage value for the period |
| usage.cost | number | Total cost in USD |
| usage.credits | number/null | Total credits consumed (null for direct-billed meters) |
| usage.start_time | string | Start of the queried range |
| usage.end_time | string | End of the queried range |
| usage.interval_hours | integer | Hours per data point (auto-calculated, max 30 points) |
| usage.data_points | array | Time-series data with value, cost, and credits per interval |
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
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
/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.
- All accounts (agent and human) see the same paid plans, whether activating a new organization or switching an existing one: Starter, Business, and Growth. Each plan is offered in a monthly and an annual interval (e.g.,
solo_monthly/solo_annual,business_v2_monthly/business_v2_annual,growth_monthly/growth_annual). - The free plan and legacy plans are not offered to new organizations; existing organizations on those plans keep their current plan.
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)
{
"result": true,
"results": 3,
"defaults": {
"pro": "solo_monthly",
"business": "business_v2_monthly"
},
"plans": [
{
"id": "solo_monthly",
"name": "Starter",
"category": "pro",
"amount": 2900
},
{
"id": "business_v2_monthly",
"name": "Business",
"category": "business",
"amount": 9900
},
{
"id": "growth_monthly",
"name": "Growth",
"category": "business",
"amount": 29900
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| results | integer | Number of available plans |
| defaults | object | Default plan IDs per category (pro, business) |
| plans | array | Array of plan detail objects |
| plans[].id | string | Plan identifier (use in subscription requests) |
| plans[].name | string | Display name |
| plans[].category | string | Plan category: "pro" (Starter), "business" (Business or Growth) |
| plans[].amount | integer | Price in cents (e.g., 2900 = $29.00) |
List Invoices
/current/org/{org_id}/billing/invoices/
Auth required. Admin or above. Returns a paginated list of invoices with hosted payment links.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| limit | integer | 10 | Number of invoices to return (1-100) |
| starting_after | string | — | 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
| Field | Type | Description |
|---|---|---|
| invoices | array | Array of invoice objects |
| invoices[].id | string | Invoice identifier (use as starting_after cursor) |
| invoices[].status | string | "draft", "open", "paid", "void", "uncollectible" |
| invoices[].currency | string | Three-letter ISO currency code (e.g., "usd") |
| invoices[].amount_due | integer | Amount due in cents |
| invoices[].amount_paid | integer | Amount paid in cents |
| invoices[].subtotal | integer | Subtotal before tax in cents |
| invoices[].total | integer | Total after tax in cents |
| invoices[].paid | boolean | Whether the invoice has been paid |
| invoices[].description | string/null | Invoice description |
| invoices[].hosted_invoice_url | string/null | URL to view and pay the invoice |
| invoices[].invoice_pdf | string/null | Direct PDF download URL |
| invoices[].period_start | string/null | Billing period start (YYYY-MM-DD HH:MM:SS UTC) |
| invoices[].period_end | string/null | Billing period end (YYYY-MM-DD HH:MM:SS UTC) |
| invoices[].created | string/null | Invoice creation timestamp (YYYY-MM-DD HH:MM:SS UTC) |
| has_more | boolean | Whether 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)
/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
| Name | Type | Required | Description |
|---|---|---|---|
| folder_name | string | Yes | URL-safe folder name for the workspace. Must be globally unique across all workspaces. |
| name | string | Yes | Display name. |
| description | string | No | Workspace description. |
| perm_join | string | Yes | Who can auto-join from the org. Values: 'Member or above' (default), 'Admin or above', 'Only Org Owners'. |
| perm_member_manage | string | Yes | Who can manage workspace members. Values: 'Member or above' (default), 'Admin or above'. |
| intelligence | string | No | Enable AI features ("true"/"false"). Defaults to "true" when omitted. Forced off on plans lacking content_ai + ai_agent, which never fails the create. |
| accent_color | string (JSON) | No | Accent color as JSON. |
| background_color1 | string (JSON) | No | Primary background color as JSON. |
| background_color2 | string (JSON) | No | Secondary 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"
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| workspace.id | string | 19-digit numeric workspace ID |
| workspace.folder_name | string | URL-safe folder name |
Error responses
| Error Code | HTTP Status | Message | Cause |
|---|---|---|---|
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
/current/org/{org_id}/list/workspaces/
Auth required. Lists accessible workspaces within the org.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| archived | string | "false" | "true" to show archived workspaces, "false" for active |
Access levels
| Role | Access | Notes |
|---|---|---|
| Owner | Full access | Sees all workspaces |
| Admin | Full access | Sees all workspaces except those restricted to perm_join = 'Only Org Owners' (unless directly a member) |
| Member | Filtered | Sees workspaces matching join permission level |
| External | Filtered | Sees 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"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| workspaces | array | Array of workspace objects |
| workspaces[].id | string | 19-digit numeric workspace ID |
| workspaces[].folder_name | string | URL-safe folder name |
| workspaces[].name | string | Display name |
| workspaces[].description | string/null | Workspace description |