Authentication & User Management Authentication methods, user CRUD, getting started patterns

Base URL: https://api.fast.io/current/ Request format: application/x-www-form-urlencoded (POST) or query string (GET) Response format: JSON

Authentication Methods

All authenticated endpoints require: Authorization: Bearer {token}

The token can be a JWT (from Basic Auth or OAuth), an API key, or a 2FA-upgraded JWT.

Method 1: Basic Auth to JWT

Send HTTP Basic Auth (email:password) to get a JWT.

GET /current/user/auth/

Authorization: Basic {base64(email:password)}

Returns auth_token (JWT). If the account has 2FA enabled, the returned token has limited scope until 2FA verification is completed.

Optional revocable=true — pass to mint a session-bound JWT that can be invalidated server-side via POST /current/user/auth/sign-out/. Recommended for browser sessions where users expect a logout button to terminate access. JWTs minted without revocable (the default) are stateless and sign-out has no effect on them — but ALL login tokens (with or without revocable) can still be killed via POST /current/user/auth/invalidate-all/, which terminates every login session for the user. See the session-termination endpoints (sign-out / invalidate-all) below.

Optional x-ve-session-cookie request header — browser clients only. Send x-ve-session-cookie: 1 (or true/yes — the same vocabulary as revocable) to ask the server to ALSO deliver the issued token in an HttpOnly, Secure, SameSite=Lax cookie scoped to the site's registrable domain (e.g. fast.io), so it is available across subdomains on that domain and the session never has to be stored anywhere JavaScript can read it between page loads. The token is still returned in the response body as usual. The browser then calls POST /current/user/auth/bootstrap/ on each page load — the only endpoint that reads the cookie — which returns that same token so the app can bring it into memory. The opt-in is only honoured as a request header; it is deliberately not accepted as a URL or body parameter. Omit the header (the default) and no cookie is set. On a 2FA-enabled account, sign-in returns a pre-2FA token and NO cookie; the cookie is issued by POST /current/user/auth/2factor/auth/{token}/ instead.

Method 2: API Keys

Long-lived tokens for service-to-service communication. Created via the API or the web UI. Used with the same Authorization: Bearer {api_key} header format as JWTs. Keys optionally support scoped permissions (scopes), agent names (agent_name), and expiration (expires). Scoped keys are enforced using the same scope system as v2.0 JWT tokens. A key created or updated without scopes now stores the explicit ["user:*:rw"] — whole-account read and write, no administration and no account settings.

Scope formats: OAuth authorization requests accept named scope strings (user, org, workspace, all_orgs, all_workspaces, all_shares). API responses return scopes as arrays of entity_type:entity_id:access_mode strings (e.g., ["org:12345:rw"]) — exactly three colon-separated parts. The entity types are user, org, workspace, share, fileshare, memory and userdetails; the entity id is * (wildcard) or a numeric id. See the OAuth 2.0 reference for full scope format details.

Access modes. The third part of a scope string is one of three modes:

ModeGrants
rRead
rwRead and write
rwaRead, write and administer

rwa implies rw, which implies r. There is no ra — administration always includes write. rwa is accepted on the ordinary entity types (user, org, workspace, share) but not on fileshare, which has no administrative verb: fileshare:{id}:rwa and any fileshare:* scope are refused when a credential is issued. userdetails is narrower still — the only issuable form is the exact userdetails:*:rw, and userdetails:*:r, userdetails:*:rwa and any numeric id are refused at grant time.

Administration is always capped by the human. rwa never grants more than the person who owns the credential currently holds — it is re-checked on every request against their live role on the entity. If they lose administrative rights on an org or workspace, a credential carrying rwa for it stops administering it.

The user:* grants:

ScopeMeaning
user:*:rWhole account, read-only — reads every entity the human can read, and writes nothing. Entity-anchored writes are refused by the entity's own scope check; account-anchored ones (creating an org, updating your account, revoking an OAuth session or all of them, minting or editing an API key) are refused with 403 and 10770, error.params.reason scope_write_required. There is no exempt route — sign-out included, so a read-only client ends its session by discarding the credential locally, or by calling POST /current/oauth/revoke/. Login, the 2FA login challenge and the OAuth token exchange are unaffected, but only because none of them runs on a scoped bearer in the first place
user:*:rwWhole account, read and writenot administration, not account settings
user:*:rwaWhole account, read, write and administer (still capped by the human's live role)

A user:* grant matches every ordinary entity type. It does not match userdetails, which is explicit-only.

The userdetails entity type. userdetails:*:rw is the only valid form — userdetails:*:r, userdetails:*:rwa and any numeric id are refused when a credential is issued. It gates exactly four operations:

No user:* grant, at any access mode, satisfies it: it must be held explicitly, or the caller must be an interactive browser login session. It is an entity type inside a credential's scopes list, not a scope type in the OAuth authorization flow, and it is never advertised in scopes_supported.

Credentials with no scopes (“legacy”). A credential is legacy when it declares no scopes claim at all: an API key created before scoped keys existed, an OAuth session minted before scoped tokens existed, and every browser login session. Legacy is therefore not a synonym for “old API key”.

Newly issued credentials are never legacy: a key created or updated without scopes stores the explicit ["user:*:rw"], and an OAuth grant for scope=user is stored the same way.

Containment — no credential may mint something broader than itself. A credential may only create or edit another credential whose scopes it already covers. Covering compares access-mode rank (rwarwr), and a wildcard id covers any id of that type while a numeric id covers only itself: org:*:rw covers org:123:r, but org:123:rw does not cover org:*:r. There is no hierarchy walkorg:1:rwa does not cover workspace:5:rw. userdetails:*:rw must be held exactly to be propagated, and an empty scope set is refused everywhere it can be submitted. A browser login session is unbounded and may mint anything the human may grant.

Method 3: OAuth 2.0 PKCE

For desktop/mobile apps and MCP-connected agents. No password passes through the agent. Access tokens last 1 hour; refresh tokens are long-lived. S256 challenge method only. See the OAuth 2.0 reference for the full flow.

Method 4: 2FA

When 2FA is enabled on an account, Basic Auth returns a limited-scope JWT. Complete authentication via POST /current/user/auth/2factor/auth/{token}/ with the 2FA code. The response contains a full-scope JWT.

What the access modes mean for existing credentials

Getting Started

Option 1: Use a Human's Existing Account (API Key)

A human creates an API key and gives it to you. You operate as that user with their permissions, org, and billing.

Human instructions: "Go to Settings > Devices & Agents > API Keys and click Create API Key. Optionally enter a memo to label the key (e.g., 'Agent access'), then click Create. Copy the key immediately -- it is only displayed once. Direct link: https://go.fast.io/settings/api-keys"

Once you have the key: Authorization: Bearer {api_key}. No further steps needed.

Option 2: Create Your Own Agent Account (Autonomous)

Create your own account to work independently. Agent accounts are ordinary Fastio accounts tagged account_type=agent — they require an email address and follow the same signup, organization, and paid-plan flow as everyone else.

  1. POST /current/user/ with email_address, password, tos_agree=true, agent=true
  2. GET /current/user/auth/ with Basic Auth to get JWT
  3. Verify email:
    • POST /current/user/email/validate/ with email — sends verification code
    • POST /current/user/email/validate/ with email and email_token — validates the code
  4. POST /current/org/create/ with domain (required, 2-63 chars lowercase alphanumeric + hyphens)
  5. Select a paid plan to activate the org via POST /current/org/{org_id}/billing/ with billing_plan (e.g. solo_monthly) — a new organization must choose a paid plan (Starter, Business, or Growth) before it can be used (see the Organizations reference)
  6. POST /current/org/{org_id}/create/workspace/ with folder_name, name, perm_join, perm_member_manage

New organizations choose a paid plan (Starter, Business, or Growth) to get started; until a paid plan is selected the org is in an upgrade-only state (the same state as an org that has exhausted its credits).

Option 3: Agent Account Invited to a Human's Org

  1. Create an agent account (steps 1-2 from Option 2)
  2. Give the human your agent's email address
  3. Human invites agent to their org or workspace
  4. Accept: POST /current/org/{org_id}/members/join/ or POST /current/workspace/{workspace_id}/members/join/
  5. You now operate within their resources with granted permissions

Option 4: PKCE Browser Login (No Password Sharing)

Most secure option. Works with SSO. No credentials pass through the agent.

  1. Agent initiates PKCE flow via POST /current/oauth/authorize/ with code_challenge, code_challenge_method=S256, client_id, redirect_uri, response_type=code
  2. User opens the returned URL in browser, signs in, approves access
  3. Browser displays authorization code — user copies it to agent
  4. Agent calls POST /current/oauth/token/ with grant_type=authorization_code, code, code_verifier
  5. Access tokens last 1 hour; refresh via POST /current/oauth/token/ with grant_type=refresh_token

Which option to choose

Compact Responses (output=)

Every endpoint that returns user objects — including your own profile (/current/user/details/), other users' profiles, and member listings on workspaces, orgs, and shares — accepts an optional output query parameter that selects the response shape. A single detail-level token may be combined with modifier tokens; specifying two detail levels (e.g. ?output=terse,standard) returns HTTP 406. When output= is omitted, responses are full and byte-for-byte unchanged.

LevelFields returned on each user (cumulative)
terseid, account_type, first_name, last_name, profile_pic
standardterse + email_address, is_anonymous, status, permissions, created, member_added_at (membership responses only), updated, invite, expires, locked, suspended, closed (last three visible to self/managers only)
fullstandard + country_code, phone_country, phone_number, 2factor, notify, sync_profile, tos_agree, valid_email, valid_phone, apps, owner_defined, parents

Use terse for mention pickers, avatar lists, creator cells, and message-author headers — it carries the identifier, display name, account type (human/agent), and profile picture, which is everything the avatar/name cells render. email_address is intentionally excluded from terse to keep PII out of the smallest shape. Use standard for member list views and account-settings summaries — it adds email_address, the caller-relative permissions role, active/pending status, invitation details for pending members, and account created/updated timestamps (now visible at standard for every user the caller can see, not just self/managers). Membership responses — the member-detail endpoint for an org, workspace, or share — also carry member_added_at at standard: the date the membership was created, distinct from created, which is the date the user's own account was created. Fastio returns it to the member themselves and to admins (and owners) of the containing org, workspace, or share; for any other caller the key is absent from the response rather than null, and it is omitted for every caller when the membership has no recorded date. It is formatted like every other response timestamp, e.g. 2026-08-26 14:03:11 UTC. Admin member-list UIs also receive the lock/suspend/close account-status chips at standard; these three fields are gated server-side to the self-view or manager-view of the target user, so non-privileged callers never see them at any tier. Use full (or omit the parameter) for the user profile screen, account settings, admin audits, and any workflow that reads phone, 2FA, TOS, anonymous-guest detection, or account-validity fields. 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.

User Creation

POST /current/user/

Create a new user account.

Auth: None (IP-throttled)

Request Parameters

ParameterTypeRequiredConstraintsDescription
email_address string Yes Valid email format; domain must accept email; must be unique User's email address. Tags (e.g., +tag) are stripped for storage and uniqueness checks, but the original is preserved.
password string Yes Must pass password validity checks Account password.
tos_agree string Yes Must be "true" Must be "true" to accept Terms of Service.
agent string No "true" or "false" Set "true" for AI agent accounts. Sets account_type to "agent" permanently for identification. It does not grant a different or free plan — agent accounts follow the same signup, organization, and paid-plan flow as everyone else.
first_name string No 2–45 characters; refused if it contains a URL or scheme: prefix, a www. or domain-like token (example.com), an IP address, or </> User's given/first name.
last_name string No 2–45 characters; refused if it contains a URL or scheme: prefix, a www. or domain-like token (example.com), an IP address, or </> User's family/last name.
phone_country string No Numeric country calling code Phone country code. Required if phone_number is provided.
phone_number string No Numeric phone number Phone number. Required if phone_country is provided.

Request Example

curl -X POST "https://api.fast.io/current/user/" \
  -d "email_address=jane.doe@example.com" \
  -d "password=$PASSWORD" \
  -d "tos_agree=true" \
  -d "first_name=Jane" \
  -d "last_name=Doe" \
  -d "agent=true"

Success Response (200 OK)

{
  "result": true
}

Response Fields

FieldTypeDescription
resultbooleantrue on success

Error Responses

Error CodeHTTP StatusMessageCause
10025406"An invalid email was supplied."Email format invalid
10025406"The email domain is invalid or cannot receive email."Email domain validation failed
162057406"Accounts cannot be registered with this email domain."The email domain, or a parent of it, is reserved and cannot be used to register an account
10026406"An invalid password was supplied."Password does not meet requirements
10394406"An invalid tos_agree value was create."TOS value not a valid boolean string
10395406"You declined to accept the terms of service."TOS set to "false"
10027406"An invalid first name was supplied to create."First name fails validation
10027406"An invalid last name was supplied to create."Last name fails validation
10163406"An invalid phone country code was supplied."Invalid phone country code
10029406"An invalid phone number was supplied."Invalid phone number
10165406"An invalid phone number or country code was supplied."Full phone number validation failed
10354401"Your attempt to create an account was not accepted."Risk/fraud check failed
10032500"We were unable to create your user account..."Internal processing failure

Notes

User Management Endpoints

POST /current/user/update/

Update the current authenticated user's profile information.

Auth: Required (JWT)

Request Parameters

All fields are optional. Only provided fields are updated.

ParameterTypeRequiredConstraintsDescription
email_address string No Valid email format; unique; domain must accept email New email address. You MUST also send current_password. An account with no password yet (password_set: false, SSO-only) is refused with 10766 — set a password through the email reset flow first. Does not take effect immediately: a confirmation link is emailed to the new address and the change applies only after it is confirmed via /current/user/email/change/. Your current email stays active and verified until then.
password string No Must pass validity checks; POST-only New password for an account that already has one — you MUST also send current_password. An account with no password yet (password_set: false on GET /current/user/details/, i.e. SSO-only) cannot set its first password here: the request is refused with 10766 and the first password is set through the email reset flow (POST /current/user/email/reset/ then POST /current/user/password/{code}/). Must be sent in the POST body — a copy in the query string is rejected, not ignored.
current_password string Conditional Must match the account's current password; POST-only Required to change the password or email_address of an account that already has a password. POST body only, never the query string.
first_name string No 2–45 characters; refused if it contains a URL or scheme: prefix, a www. or domain-like token (example.com), an IP address, or </> Updated given/first name.
last_name string No 2–45 characters; refused if it contains a URL or scheme: prefix, a www. or domain-like token (example.com), an IP address, or </> Updated family/last name.
phone_country string No Numeric country code; 2FA must be disabled first Updated phone country code. Pass "null" or empty to clear.
phone_number string No Numeric phone number; 2FA must be disabled first Updated phone number. Pass "null" or empty to clear.
owner_defined string (JSON) No Must be valid JSON if provided Custom owner-defined properties. Pass null or empty to clear.

Request Example

curl -X POST "https://api.fast.io/current/user/update/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "first_name=Jane" \
  -d "last_name=Smith"

Success Response (200 OK)

{
  "result": true,
  "sessions_invalidated": true,
  "auth_token": "{jwt_token}"
}

Both extra fields are optional and appear only when the password was changed:

FieldWhen presentMeaning
sessions_invalidatedAlways, when the password changedOther sessions on this account have been signed out. Your own credential may be among them — see auth_token.
auth_tokenWhen the password changed and you authenticated with a sign-in session tokenA replacement session token. Your previous one is no longer valid; use this for subsequent requests.
email_send_failedA combined password + email_address change whose confirmation email could not be sentThe password change still succeeded; the email change did not start.

If sessions_invalidated is true but no auth_token is returned, you authenticated with a credential that was not invalidated (an API key or an OAuth access token), so no replacement is needed and you can keep using it. Do not treat a missing auth_token as a failure.

Error Responses

Error CodeHTTP StatusMessageCause
10025406"An invalid email was supplied to update."Invalid email format
10025406"The email domain is invalid or cannot receive email."Invalid email domain
10025409"The email you specified is not available."Email already in use
20544500"We could not send the confirmation email; please try again."The email-change confirmation could not be sent, so the email change was not started; any other fields in the same request were still applied
10164406"You must disable 2-Factor before updating your phone."2FA enabled when trying to change phone
10026406"An invalid password was supplied to update."Invalid password
10026406"The password must be sent in the POST body, not the query string."password was supplied as a query parameter
10770403"Your credential is read-only and is not authorized to make changes."The calling credential holds no write-capable scope anywhere — user:*:r, or a set every entry of which is :r. error.params.reason is scope_write_required
10769403"This operation changes account credentials and requires the "userdetails:*:rw" scope."password or a changed email_address sent with a credential that does not explicitly hold userdetails:*:rw (see the note below). error.params.reason is userdetails_scope_required
10766406"This account has no password yet. Set the first password through the email reset flow: request a code with POST /current/user/email/reset/ and complete it with POST /current/user/password/{code}/."password sent for an account that has no password (SSO-only) — the first password is set through the email reset flow, never through a signed-in session
10766406"This account has no password yet. Set a password through the email reset flow first (POST /current/user/email/reset/, then POST /current/user/password/{code}/), then change the email with current_password."A changed email_address sent for an account that has no password (SSO-only) — set a password first, then change the email
10759403"Your current password is required and must be correct to change your password or email."Changing the password or email of a password-having account without a valid current_password
10027406"An invalid first name was supplied to update."Invalid first name
10027406"An invalid last name was supplied to update."Invalid last name
10731406"Owner-defined properties must be valid JSON."Invalid JSON in owner_defined
10354406"Your request was not accepted."The new email address failed a risk check. The message is deliberately non-specific and there is nothing to correct in the request itself — this is the same code as the signup-time risk rejection, in a different context

Notes


POST /current/user/close/

Close (soft-delete) the current user's account.

Auth: Required (JWT)

Request Parameters

ParameterTypeRequiredDescription
email_address string Yes Must match the user's current email address (confirmation).
dryrun string No If truthy, checks eligibility without closing the account.

Request Example

curl -X POST "https://api.fast.io/current/user/close/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "email_address=jane.doe@example.com"

Success Response (202 Accepted)

{
  "result": true
}

Dry Run Response (Cannot Close, 202 Accepted)

{
  "result": false
}

Error Responses

Error CodeHTTP StatusMessageCause
10024404"User not found to close."User object invalid
10025406"An invalid email was supplied to close account."Invalid email format
10025406"An incorrect email was supplied to close account."Email does not match user's email
159788406"Cannot close user account that owns active organizations..."User owns active organizations

Notes


POST /current/user/email/

Deprecated. This endpoint previously reported whether an email was already registered, which let anyone enumerate Fastio accounts. It no longer performs any account lookup: for any well-formed email it returns a uniform 202 Accepted / result: true. It is retained only so existing callers keep receiving a success response. To handle an already-registered email, just call signup (POST /user/) — it notifies the existing account and returns the same success as a new signup.

Auth: None (IP-throttled)

Request Parameters

ParameterTypeRequiredDescription
email string Yes Email address (format-validated only; not looked up).

Request Example

curl -X POST "https://api.fast.io/current/user/email/" \
  -d "email=jane.doe@example.com"

Response (202 Accepted) — always, regardless of whether the email is registered

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
10022406"You provided an invalid email to check."Invalid email format or missing

Notes


POST /current/user/email/reset/

Request a password reset email.

Auth: None (IP-throttled)

Request Parameters

ParameterTypeRequiredDescription
email string Yes Email address of the account.

Request Example

curl -X POST "https://api.fast.io/current/user/email/reset/" \
  -d "email=jane.doe@example.com"

Success Response (202 Accepted)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
10022406"You provided an invalid email to check."Invalid email format
20544500"We were unable to send a verification email."Email send failure

Notes


POST /current/user/email/validate/

Send or validate an email verification code. Two-step flow.

Auth: Required (JWT)

Mode 1: Send Verification Code

When email_token is NOT provided, sends a new validation code to the user's email.

ParameterTypeRequiredDescription
email string Yes Must match the authenticated user's email address.

Mode 2: Validate Code

When email_token IS provided, validates the code and marks the email as verified.

ParameterTypeRequiredDescription
email string Yes Must match the authenticated user's email address.
email_token string Yes Verification code received via email.

Request Example (Send Code)

curl -X POST "https://api.fast.io/current/user/email/validate/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "email=jane.doe@example.com"

Request Example (Validate Code)

curl -X POST "https://api.fast.io/current/user/email/validate/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "email=jane.doe@example.com" \
  -d "email_token=123456"

Success Response (202 Accepted)

{
  "result": true
}

Side Effects

Error Responses

Error CodeHTTP StatusMessageCause
10011401"Your credentials were not supplied or invalid."User not authenticated
10037406"Your email address is already verified."Email already verified
10023409"Your credentials do not match the email you provided."Email mismatch with authenticated user
10033406"You provided an invalid or expired token to validate email."Invalid or expired code
10199401"Provided code has expired, get a new code and try again."Code expired

POST /current/user/email/change/

Confirm a pending email change. Consumes the one-time confirmation token from the link that was emailed to the new address when the change was requested via /current/user/update/, and applies the new email.

Auth: Required (JWT). The signed-in user must be the account the change was requested for.

Request Parameters

ParameterTypeRequiredDescription
token string Yes The one-time confirmation token from the confirmation link.

Request Example

curl -X POST "https://api.fast.io/current/user/email/change/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "token={confirmation_token}"

Success Response (202 Accepted)

{
  "result": true
}

Side Effects

Error Responses

Error CodeHTTP StatusMessageCause
10755406"There is no pending email change to confirm."No pending change exists for this account
10033401"The confirmation link is invalid or has expired."Token invalid, expired, or already used
10023401"This confirmation link does not belong to the signed-in account."Token belongs to a different account than the one signed in
10025409"That email address is no longer available."The pending email was claimed by another account before confirmation
10032500"There was an internal error applying your email change."The change could not be applied

Notes


POST /current/user/password/{code}/

Set a new password using a password reset code.

Auth: None (code-based authentication)

Path Parameters

ParameterTypeRequiredDescription
{code} string Yes Password reset code from the reset email.

Request Parameters

ParameterTypeRequiredDescription
password1 string Yes New password.
password2 string Yes New password confirmation. Must match password1.

Request Example

curl -X POST "https://api.fast.io/current/user/password/abc123def456/" \
  -d "password1=NewSecureP@ss" \
  -d "password2=NewSecureP@ss"

Success Response (202 Accepted)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
10197401"An invalid code was provided, cannot reset password."Invalid code format
10198401"Provided code was not found or expired, cannot reset password."Code not found or wrong type
10199401"Provided code has expired, get a new code and try again."Code expired
10200409"Provided code belongs to another user account and cannot be used."Code/user mismatch
10201404"The provided code belongs to an invalid user."User not found for code
10202409"The provided passwords don't match."password1 and password2 differ
10204406"Both password fields must be provided and match."Missing password fields
10203500"The provided password could not be processed..."Encryption failure
10204500"The provided password could not be processed..."The new password could not be written
10204500"The provided password could not be processed..."The account could not be reloaded before its sessions were invalidated
10204500"The provided password could not be processed..."The reset code could not be claimed because the datastore did not answer; nothing was consumed, retry

Notes


GET /current/user/password/{code}/details/

Get details of a password reset code (check if valid/expired).

Auth: None (code-based)

Path Parameters

ParameterTypeRequiredDescription
{code} string Yes Password reset code to check.

Request Example

curl -X GET "https://api.fast.io/current/user/password/abc123def456/details/"

Success Response (200 OK)

{
  "result": true,
  "email": "jane.doe@example.com"
}

Response Fields

FieldTypeDescription
response.emailstringThe email address associated with the reset code.

Error Responses

Error CodeHTTP StatusMessageCause
10197401"An invalid code was provided, cannot reset password."Invalid code format
10198401"Provided code was not found or expired, cannot reset password."Code not found
10199401"Provided code has expired, get a new code and try again."Code expired
10200409"Provided code belongs to another user account..."Code/user mismatch
10207423"The account has been restricted and cannot be updated."Account locked/suspended/closed

GET /current/user/phone/{country_code}-{phone_number}/

Validate a phone number and country code combination.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{country_code}-{phone_number} string Yes Country code and phone number separated by a hyphen (e.g., 1-5551234567).

Request Example

curl -X GET "https://api.fast.io/current/user/phone/1-5551234567/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (202 Accepted)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
10022406"You provided an invalid phone number to check."Invalid format
10163406"An invalid phone country code was supplied."Invalid country code
10029406"An invalid phone number was supplied."Invalid phone number
10165406"An invalid phone number or country code was supplied."Full number validation failed

GET /current/user/pin/

Get the user's support PIN and identity verification hash.

Auth: Required (JWT)

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "supportcode": "1234",
  "intercom": "a1b2c3d4e5f6..."
}

Response Fields

FieldTypeDescription
response.supportcodestring4-digit support PIN. Defaults to "0000" if not set.
response.intercomstringHMAC-SHA256 identity-verification hash of your user ID, for authenticating you to the support widget.

Error Responses

Error CodeHTTP StatusMessageCause
10023404"Unable to fetch the user details."User not found
10541500"Internal temporary error, please try again later."Internal error

GET|POST /current/user/sso/signin/{provider}/

SSO (Single Sign-On) authentication flow.

Auth: None (IP-throttled)

Path Parameters

ParameterTypeRequiredDescription
{provider} string Yes SSO provider name: google or microsoft.

GET: Get SSO Redirect URL

Returns the OAuth2 authorization URL for the specified provider.

curl -X GET "https://api.fast.io/current/user/sso/signin/google/"

To request a session-bound JWT after the SSO flow completes:

curl -X GET "https://api.fast.io/current/user/sso/signin/google/?revocable=true"

Optional Query Parameters (GET only)

ParameterTypeDefaultDescription
revocable boolean false When true, the JWT issued at the end of the SSO flow is session-bound and can be invalidated server-side via POST /current/user/auth/sign-out/. The flag rides through the OAuth round-trip inside the HMAC-signed state token. Recommended for browser SSO logins.
return_url string (unset) Optional https:// URL to redirect to after the SSO callback completes. Must be a trusted Fastio domain.

GET Response (200 OK)

{
  "result": true,
  "provider": "google",
  "redirect_url": "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=...",
  "return_url": "https://fast.io/sso/callback/google"
}

Response Fields

FieldTypeDescription
response.providerstringThe provider name.
response.redirect_urlstringURL to redirect the user to for SSO authentication.
response.return_urlstringCallback URL the provider will redirect back to.

POST: Process SSO Callback

Processes the OAuth2 callback with the authorization code from the provider.

ParameterTypeRequiredDescription
code string Yes Authorization code from the SSO provider.
state string Yes State token for CSRF protection.

POST Response (200 OK)

{
  "result": true,
  "provider": "google",
  "email": "user@example.com",
  "token": "{jwt_token}",
  "2factor": false,
  "account_created": true
}

Response Fields

FieldTypeDescription
response.providerstringThe provider that authenticated the user.
response.emailstringThe email address on the authenticated account.
response.tokenstringThe issued JWT. Send it as Authorization: Bearer {token}.
response.2factorbooleanWhether the account has 2FA enabled. When true, complete the 2FA step before the token has full access.
response.account_createdbooleanPresent and true only when this exchange created the account — i.e. a first-time SSO signup. Omitted entirely for a returning sign-in, so treat a missing field as false.
response.redirect_after_loginstringOnly present when a return_url was supplied on the GET step; the URL to send the user to after login completes.

Error Responses

Error CodeHTTP StatusMessageCause
10041406"An invalid provider name was supplied."Invalid provider name format
10226406"An unknown provider name was supplied."Provider not in allowed list
10530406"Cookies must be enabled and passed to this API."Missing state cookie
10260401"Permission was not granted by the provider."OAuth error returned from provider
10230401"Invalid or missing input in a required field was received."Missing code or state
145237401"Accounts cannot be registered with this email domain."First-time SSO signup only: the provider-asserted email is at a reserved domain, or a subdomain of one, so no account is created. An account that already exists on such a domain can still sign in

Notes


GET /current/user/assets/

List available user asset metadata types (e.g., profile photo specifications).

Auth: None

Request Example

curl -X GET "https://api.fast.io/current/user/assets/"

Notes


GET /current/user/available_profiles/

Check what profile types (orgs, workspaces, shares) the current user has access to.

Auth: Required (JWT)

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "has_orgs": true,
  "has_workspaces": true,
  "has_shares": false,
  "has_pending_invitations": false
}

Response Fields

FieldTypeDescription
response.has_orgsbooleanWhether the user has access to any organizations.
response.has_workspacesbooleanWhether the user has access to any workspaces.
response.has_sharesbooleanWhether the user has access to any shares.
response.has_pending_invitationsbooleanWhether the user has any pending invitations awaiting their explicit acceptance. Computed for verified-email accounts only (false otherwise).

Error Responses

Error CodeHTTP StatusMessageCause
10023404"Unable to fetch the user details."User not found

GET /current/user/{user_id}/details/

Get user profile details.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{user_id} string No 19-digit user ID. If omitted, returns the current user's details.

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "user": {
    "id": "1234567890123456789",
    "account_type": "human",
    "email_address": "jane.doe@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "locked": false,
    "profile_pic": "https://assets.fast.io/..."
  }
}

Response Fields

FieldTypeDescription
response.user.idstring19-digit user ID.
response.user.account_typestring"human" or "agent".
response.user.email_addressstringUser's email address.
response.user.first_namestringGiven name.
response.user.last_namestringFamily name.
response.user.lockedbooleanWhether the account is locked.
response.user.profile_picstringProfile photo URL.

Self-Only Fields (included only when viewing your own profile)

FieldTypeDescription
2factorbooleanWhether 2FA is enabled.
closedbooleanWhether the account is closed.
country_codestringCountry of residence.
createdstringRegistration date.
password_setbooleanWhether the account has a password set (false = SSO-only). Self/manager view only.
phone_countrystringPhone country code.
phone_numberstringPhone number.
suspendedbooleanSuspension status.
tos_agreestringToS agreement date.
updatedstringLast profile update time.
valid_emailbooleanEmail verified status.
valid_phonebooleanPhone verified status.

Error Responses

Error CodeHTTP StatusMessageCause
10023404"Unable to fetch the user details."User not found

GET /current/user/me/autosync/{state}/

Enable or disable profile photo auto-synchronization from SSO providers.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{state} string Yes "enable" or "disable".

Request Example

curl -X GET "https://api.fast.io/current/user/me/autosync/enable/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
135405500"There was an internal error processing your request..."Commit failure

GET /current/user/me/allowed/

Check if the user's country (based on IP geolocation) allows creating shares or organizations.

Auth: None (IP-throttled)

Request Example

curl -X GET "https://api.fast.io/current/user/me/allowed/"

Success Response (200 OK)

{
  "result": true,
  "allowed": true
}

Response Fields

FieldTypeDescription
response.allowedbooleanWhether the user's location allows resource creation.
response.reasonsarrayArray of blocked reason strings. Only present when allowed is false.

GET /current/user/me/limits/orgs/

Check free organization creation eligibility.

Auth: Required (JWT)

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "can_create_free_org": false,
  "existing_free_orgs": 0,
  "cooldown_remaining": 0,
  "max_free_orgs": 1,
  "reason": "The free plan is no longer available. Please choose a paid plan.",
  "free_trial_eligible": true,
  "trial_days": 14
}

The free plan is retired for new organizations, so can_create_free_org is false and a reason is returned. (These field NAMES are unchanged. The plan identifier itself is now reported as unpaid rather than free — an organization with no active subscription is on the unpaid tier.) New organizations select a paid plan (Starter, Business, or Growth) instead. While the free plan is closed, existing_free_orgs and cooldown_remaining are not meaningful (reported as 0/null).

The free_trial_eligible and trial_days fields describe whether a NEW org this user creates can start a free trial of a paid plan (vs. buying immediately). A free trial is only available on a user's first organization, so at this pre-org stage free_trial_eligible is true only when the user owns no organization at all — once the user owns (or has ever owned) any organization, every later org is permanently ineligible regardless of elapsed time. A secondary per-user cooldown (60 days on modern plans, anchored at trial start) also applies on top of this. Use these fields to render the plan-selection cards ("Start N-day free trial" vs. "Buy now") before any org exists. When free_trial_eligible is false, a no_trial_reason string is also returned explaining why; a trial_available_at timestamp is included only when the block is the cooldown (the first-org block is permanent, so it has no future date).

Response Fields

FieldTypeDescription
response.can_create_free_orgbooleanWhether the user can create a free organization. The free plan is retired for new orgs, so this is false.
response.existing_free_orgsintegerNumber of existing free organizations owned by the user.
response.cooldown_remainingintegerSeconds remaining before next creation is allowed.
response.max_free_orgsintegerMaximum number of free organizations allowed.
response.reasonstringReason creation is not allowed. Only present when can_create_free_org is false.
response.free_trial_eligiblebooleanWhether a new org this user creates can start a free trial of a paid plan. true only when the user owns no organization at all (a trial is only ever available on a user's first org); false and permanent thereafter, subject also to the per-user cooldown.
response.trial_daysintegerLength of the free trial in days for the default paid plan.
response.no_trial_reasonstringWhy the user is not trial-eligible. Present only when free_trial_eligible is false and a reason exists.
response.trial_available_atstringCanonical Y-m-d H:i:s UTC timestamp of when the user next becomes trial-eligible. Present only when blocked by the per-user cooldown — absent when the user already owns an organization, since that block is permanent.

Error Responses

Error CodeHTTP StatusMessageCause
141088404"Unable to fetch the user."User not found

GET /current/user/me/list/shares/

List all shares accessible to the current user.

Auth: Required (JWT)

Query Parameters

ParameterTypeRequiredDefaultDescription
archived string No "false" "true" to show archived shares, "false" to show non-archived.
limit integer No 100 Page size (1–500). An out-of-range value is rejected as invalid input.
offset integer No 0 Number of items to skip before the returned page.

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "shares": [
    {
      "id": "1234567890123456789",
      "name": "Project Files",
      "type": "send",
      "archived": false
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 50,
    "offset": 0,
    "has_more": false
  }
}

Response Fields

FieldTypeDescription
response.sharesarrayArray of share resource objects for the current page. Each includes parent workspace and org info.
response.paginationobjectPagination metadata: total (count of all matching shares before paging), limit, offset, and has_more (boolean — true when more items remain beyond this page).

Notes


GET /current/user/{user_id}/assets/

List set assets (e.g., profile photo) for a user.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{user_id} string Yes 19-digit numeric user ID.

Request Example

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

POST|DELETE /current/user/{user_id}/assets/{asset_name}/

Upload or delete a user asset.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{user_id} string Yes 19-digit numeric user ID.
{asset_name} string Yes Asset type name (e.g., profile_pic).

POST: Upload Asset

Multipart form data with exactly one file upload.

ParameterTypeRequiredDescription
(file) file Yes The asset file. Exactly one file must be included.
metadata array No Optional metadata. Must be a valid array if provided.

DELETE: Delete Asset

No request body required.

Error Responses

Error CodeHTTP StatusMessageCause
10586400"Only user may modify."Non-owner attempting to modify
10418400"Asset upload missing"No file in POST request
156780406"metadata invalid"Invalid metadata parameter

Notes


GET|HEAD /current/user/{user_id}/assets/{asset_name}/read/

Read the binary content of a user asset.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{user_id} string Yes 19-digit numeric user ID.
{asset_name} string Yes Asset type name (e.g., profile_pic).

Notes

Installed Apps

Track the desktop/mobile apps and agents a user has installed on their account. All four endpoints are user-authenticated and scoped to the calling user; installations are keyed by a caller-defined app_id.

An installation object has this shape everywhere it is returned:

FieldTypeDescription
idstring19-digit installation ID.
app_idstringCaller-defined app identifier this installation belongs to.
app_versionstring or nullApp version last reported, or null if never provided.
platformstring or nullPlatform string last reported (e.g. macos, windows, ios), or null.
statusstring"installed" or "uninstalled".
installed_atstringCanonical Y-m-d H:i:s UTC timestamp of first install.
uninstalled_atstring or nullCanonical Y-m-d H:i:s UTC timestamp of last uninstall, or null if currently installed.
last_heartbeatstring or nullCanonical Y-m-d H:i:s UTC timestamp of the last heartbeat/install check-in, or null.
metadataobject or nullArbitrary caller-defined JSON metadata, or null.

GET /current/user/apps/

List all app installations for the authenticated user (both installed and previously uninstalled).

Auth: Required (JWT)

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "apps": [
    {
      "id": "1234567890123456789",
      "app_id": "com.example.desktop",
      "app_version": "1.4.2",
      "platform": "macos",
      "status": "installed",
      "installed_at": "2026-07-07 16:37:29 UTC",
      "uninstalled_at": null,
      "last_heartbeat": "2026-07-07 18:02:10 UTC",
      "metadata": null
    }
  ]
}

Response Fields

FieldTypeDescription
response.appsarrayArray of installation objects (see shape above).

Error Responses

Error CodeHTTP StatusMessageCause
227759500"Internal error initializing app installations."Backend unavailable
218645404"No app installations found."Installation lookup failed

Notes


POST /current/user/apps/install/

Register an app installation for the authenticated user. Creates a new record, reinstalls a previously uninstalled app, or updates the version/platform (and refreshes the heartbeat) on an existing installation — all keyed by app_id.

Auth: Required (JWT)

Request Parameters

ParameterTypeRequiredDescription
app_idstringYesCaller-defined app identifier. Must not be blank.
app_versionstringNoApp version string.
platformstringNoPlatform string (e.g. macos, windows, ios, android).
metadatastring (JSON)NoArbitrary JSON object of caller-defined metadata.

Request Example

curl -X POST "https://api.fast.io/current/user/apps/install/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "app_id=com.example.desktop" \
  -d "app_version=1.4.2" \
  -d "platform=macos"

Success Response (200 OK)

{
  "result": true,
  "installation": {
    "id": "1234567890123456789",
    "app_id": "com.example.desktop",
    "app_version": "1.4.2",
    "platform": "macos",
    "status": "installed",
    "installed_at": "2026-07-07 16:37:29 UTC",
    "uninstalled_at": null,
    "last_heartbeat": "2026-07-07 16:37:29 UTC",
    "metadata": null
  }
}

Response Fields

FieldTypeDescription
response.installationobjectThe created or updated installation object (see shape above).

Error Responses

Error CodeHTTP StatusMessageCause
296856 / 210782 / 224953 / 296358406Variousapp_id blank/missing/too long → 296856; app_version too long → 210782; platform too long → 224953; metadata not valid JSON → 296358
246127500"Internal error initializing app installations."Backend unavailable
217449404"Failed to save app installation."Persisting the installation failed

Notes


POST /current/user/apps/uninstall/

Mark an app installation as uninstalled.

Auth: Required (JWT)

Request Parameters

ParameterTypeRequiredDescription
app_idstringYesCaller-defined app identifier of the installation to uninstall. Must not be blank.

Request Example

curl -X POST "https://api.fast.io/current/user/apps/uninstall/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "app_id=com.example.desktop"

Success Response (200 OK)

{
  "result": true,
  "installation": {
    "id": "1234567890123456789",
    "app_id": "com.example.desktop",
    "app_version": "1.4.2",
    "platform": "macos",
    "status": "uninstalled",
    "installed_at": "2026-07-07 16:37:29 UTC",
    "uninstalled_at": "2026-07-07 19:15:44 UTC",
    "last_heartbeat": "2026-07-07 18:02:10 UTC",
    "metadata": null
  }
}

Response Fields

FieldTypeDescription
response.installationobjectThe updated installation object; status is now uninstalled.

Error Responses

Error CodeHTTP StatusMessageCause
234003406Variousapp_id blank/missing or too long
226304404"No installation record found for this app."No installation exists for this app_id
233650409"This app is already uninstalled."Installation already in the uninstalled state
237113500"Failed to save uninstall status."Persisting the change failed

POST /current/user/apps/heartbeat/

Record a periodic liveness check-in from an installed app, updating last_heartbeat and optionally the app version.

Auth: Required (JWT)

Request Parameters

ParameterTypeRequiredDescription
app_idstringYesCaller-defined app identifier of the installed app. Must not be blank.
app_versionstringNoUpdated app version string. When provided, it replaces the stored version.

Request Example

curl -X POST "https://api.fast.io/current/user/apps/heartbeat/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "app_id=com.example.desktop" \
  -d "app_version=1.4.3"

Success Response (200 OK)

{
  "result": true,
  "installation": {
    "id": "1234567890123456789",
    "app_id": "com.example.desktop",
    "app_version": "1.4.3",
    "platform": "macos",
    "status": "installed",
    "installed_at": "2026-07-07 16:37:29 UTC",
    "uninstalled_at": null,
    "last_heartbeat": "2026-07-07 20:41:03 UTC",
    "metadata": null
  }
}

Response Fields

FieldTypeDescription
response.installationobjectThe updated installation object with a refreshed last_heartbeat.

Error Responses

Error CodeHTTP StatusMessageCause
276731 / 272675406Variousapp_id blank/missing/too long → 276731; app_version too long → 272675
245439404"No installation record found for this app."No installation exists for this app_id
231483409"Cannot heartbeat an uninstalled app."Installation is in the uninstalled state
237563500"Failed to save heartbeat."Persisting the change failed

Notes

Invitations

GET /current/user/invitation/{invitation_id}/details/

Get details for a specific invitation.

Auth: Required (JWT)

Path Parameters

ParameterTypeRequiredDescription
{invitation_id} string Yes Invitation ID (numeric) or invitation key (alphanumeric).

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "invitation": {
    "id": "1234567890123456789",
    "invitation_key": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd",
    "inviter": "Jane Doe",
    "invitee_email": "newuser@example.com",
    "entity_type": "workspace",
    "workspace": {
      "id": "9876543210987654321",
      "name": "Marketing Team"
    },
    "state": "pending",
    "created": "2025-01-15 10:30:00 UTC",
    "expires": "2025-01-22 10:30:00 UTC"
  },
  "owner": {
    "id": "1111111111111111111",
    "account_type": "human",
    "email_address": "admin@example.com",
    "first_name": "Admin",
    "last_name": "User",
    "profile_pic": "https://assets.fast.io/..."
  },
  "org": {
    "id": "2222222222222222222",
    "name": "Example Org"
  }
}

Response Fields

FieldTypeDescription
response.invitationobjectInvitation resource. Embeds entity_type and the entity object (org/workspace/share). Includes invitation_key because this is the invitee's own authenticated view.
response.ownerobjectUser resource of the profile owner.
response.orgobject or nullOrg resource if the invitation is for an org-owned entity.

Error Responses

Error CodeHTTP StatusMessageCause
10618406"An invalid invitation ID was supplied."Invalid ID format
10630406"Invitation not found."Invitation does not exist
159135500"Failed to load the invitation profile or its owner."Profile or owner load failure

Notes


GET /current/user/invitation/{invitation_id}/public/details/

Get public details for an invitation without authentication.

Auth: None (IP-throttled)

Path Parameters

ParameterTypeRequiredDescription
{invitation_id} string Yes Invitation ID (numeric) or invitation key (alphanumeric).

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "invitation": {
    "id": "1234567890123456789",
    "state": "pending"
  },
  "owner": {
    "id": "1111111111111111111",
    "account_type": "human",
    "first_name": "Admin",
    "last_name": "User"
  },
  "org": {
    "id": "2222222222222222222",
    "name": "Example Org"
  }
}

Notes


POST /current/user/invitation/{invitation_id}/accept/

Accept a single pending invitation by its ID. Self-service: authorized by the authenticated invitee's verified email (or user ID), so the secret invitation key is not required. Use this to act on the invitations returned by GET /current/user/invitations/list/ — for example from a no-org landing page where the user has no email-link token.

Auth: Required (JWT, validated email)

Path Parameters

ParameterTypeRequiredDescription
{invitation_id} string Yes The invitation id from the invitations list/details response.

Request Example

curl -X POST "https://api.fast.io/current/user/invitation/1234567890123456789/accept/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true,
  "invitation": {
    "id": "1234567890123456789",
    "inviter": "Jane Doe",
    "invitee_email": "newuser@example.com",
    "entity_type": "workspace",
    "workspace": {
      "id": "9876543210987654321",
      "name": "Marketing Team"
    },
    "state": "accepted",
    "created": "2025-01-15 10:30:00 UTC",
    "expires": "2025-01-22 10:30:00 UTC"
  }
}

Response Fields

FieldTypeDescription
response.invitationobjectThe updated invitation resource; state is now accepted.

Error Responses

Error CodeHTTP StatusMessageCause
10618406"An invalid invitation ID was supplied."Malformed ID
10631406"This invitation can no longer be accepted."Already accepted/declined, or expired
10630404"Invitation not found."No such invitation
127827401"You are not authorized to act on this invitation."The invitation is not addressed to the authenticated user

Notes


POST /current/user/invitation/{invitation_id}/decline/

Decline a single pending invitation by its ID. Self-service counterpart to the accept endpoint; authorized by the authenticated invitee's verified email (or user ID), no invitation key required.

Auth: Required (JWT, validated email)

Path Parameters

ParameterTypeRequiredDescription
{invitation_id} string Yes The invitation id from the invitations list/details response.

Request Example

curl -X POST "https://api.fast.io/current/user/invitation/1234567890123456789/decline/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true,
  "invitation": {
    "id": "1234567890123456789",
    "inviter": "Jane Doe",
    "invitee_email": "newuser@example.com",
    "entity_type": "workspace",
    "workspace": {
      "id": "9876543210987654321",
      "name": "Marketing Team"
    },
    "state": "declined",
    "created": "2025-01-15 10:30:00 UTC",
    "expires": "2025-01-22 10:30:00 UTC"
  }
}

Response Fields

FieldTypeDescription
response.invitationobjectThe updated invitation resource; state is now declined.

Error Responses

Error CodeHTTP StatusMessageCause
10618406"An invalid invitation ID was supplied."Malformed ID
10631406"This invitation can no longer be declined."Already accepted/declined
10630404"Invitation not found."No such invitation
127827401"You are not authorized to act on this invitation."The invitation is not addressed to the authenticated user

Notes


POST /current/user/invitations/acceptall/

Accept all pending invitations.

Auth: Required (JWT)

Request Parameters

ParameterTypeRequiredDescription
invitation_key string No Optional invitation key. If the user's email is not validated, this key can identify invitations.

Request Example

curl -X POST "https://api.fast.io/current/user/invitations/acceptall/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true
}

Notes


GET /current/user/invitations/list/

List all pending invitations for the current user.

Auth: Required (JWT)

Query Parameters

ParameterTypeRequiredDescription
invitation_key string No Optional invitation key for users without validated email.

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "invitations": [
    {
      "id": "1234567890123456789",
      "invitation_key": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd",
      "inviter": "Jane Doe",
      "invitee_email": "newuser@example.com",
      "entity_type": "workspace",
      "workspace": {
        "id": "9876543210987654321",
        "name": "Marketing Team"
      },
      "state": "pending",
      "created": "2025-01-15 10:30:00 UTC",
      "expires": "2025-01-22 10:30:00 UTC"
    }
  ]
}

Response Fields

FieldTypeDescription
response.invitationsarrayArray of invitation resource objects.

Notes

User Authentication Endpoints

GET /current/user/auth/

Authenticate via HTTP Basic Auth. Returns JWT token.

Auth: HTTP Basic Auth (email:password)

Query Parameters

ParameterTypeRequiredDefaultDescription
expires integer No Server default Custom JWT expiration time in seconds from now. Capped at one year; a larger value is rejected with 10454.
revocable boolean No false When true, the issued JWT is session-bound and can be invalidated server-side via POST /current/user/auth/sign-out/. Recommended for browser sessions. Tokens minted without this flag are stateless and live their full TTL — sign-out has no effect on them.

Request Headers

HeaderTypeRequiredDefaultDescription
x-ve-session-cookie boolean No (absent) Browser clients only. When truthy (1, true or yes — the same vocabulary as revocable), the issued token is ALSO returned in an HttpOnly, Secure, SameSite=Lax cookie scoped to the site's registrable domain (e.g. fast.io), with the same lifetime as the token. auth_token is still returned in the body. On later page loads, bring the cookie's token back into memory with POST /current/user/auth/bootstrap/. The opt-in is only honoured as a request header — there is no URL or body parameter equivalent. On a 2FA-enabled account no cookie is set here — see the notes below.

Request Example

curl -X GET "https://api.fast.io/current/user/auth/" \
  -u "jane.doe@example.com:$PASSWORD"

To request a revocable session-bound JWT (recommended for browser logins):

curl -X GET "https://api.fast.io/current/user/auth/?revocable=true" \
  -u "jane.doe@example.com:$PASSWORD"

To additionally receive the token in an HttpOnly session cookie (browser logins that keep the token out of client storage):

curl -X GET "https://api.fast.io/current/user/auth/" \
  -u "jane.doe@example.com:$PASSWORD" \
  -H "x-ve-session-cookie: 1" \
  --cookie-jar cookies.txt

Success Response (200 OK)

{
  "result": true,
  "expires_in": 86400,
  "auth_token": "{jwt_token}",
  "2factor": false
}

Response Fields

FieldTypeDescription
response.expires_inintegerJWT token expiration time in seconds.
response.auth_tokenstringJWT access token. If 2FA is enabled, has twofactor scope (restricted). Otherwise has user scope (full access).
response.2factorbooleantrue if 2FA is enabled. Token has limited scope until 2FA verification is completed.

Error Responses

Error CodeHTTP StatusMessageCause
10454405"The expires time specified is invalid."Invalid expires parameter
10001401"Your credentials were not supplied or invalid."Missing Basic Auth header
10004401"Username is not valid."Invalid email format
10005401"Password is not valid."Invalid password format
10008401"Your credentials supplied are invalid."Wrong password, unrecognized email, or SSO-only account (no password set) — identical response and matched timing prevent account enumeration. Carries error.params.attempts_remaining + attempts_max when known
10105401"Your account is suspended..."Account suspended (only after a correct password)
10104401"Your account is locked..."Account locked
10106401"Your account is suspended due to abuse."Account flagged for abuse
10103401"Your account is closed by you."Account closed
10103401"This account has not been claimed yet."The address belongs to a placeholder account created by an invitation that has never been claimed. Same code as "account closed" — the message is the only thing that distinguishes them, so branch on the message, not the code. The remedy is different too: this one clears by accepting the invitation, not by contacting support
10760429"Too many failed sign-in attempts. Try again in N minutes."Too many consecutive failed sign-in attempts for this account — a temporary, self-clearing lockout

Notes


POST /current/user/auth/bootstrap/

Return the session token held in the browser's HttpOnly cookie. A browser that signed in with the x-ve-session-cookie header calls this on page load to bring that token into memory, then uses it in the Authorization header for every other request.

This is the ONLY endpoint in the API that authenticates from a cookie — every other endpoint still requires Authorization: Bearer {token}, unchanged. The point of the arrangement is that the durable session credential never has to live anywhere JavaScript can read it, except in memory once bootstrap hands it back.

Auth: The HttpOnly session cookie ONLY, sent automatically by the browser. A request that carries an Authorization header instead is rejected with 401 — a caller that already holds a token does not need to bootstrap.

Rate Limited: Yes (per user)

Request Parameters: None.

Request Example

curl -X POST "https://api.fast.io/current/user/auth/bootstrap/" \
  --cookie cookies.txt

Success Response (200 OK)

{
  "result": true,
  "id": "1234567890123456789",
  "auth_token": "{jwt_token}",
  "expires_in": 2591990
}

Response Fields

FieldTypeDescription
response.idstringThe 19-digit numeric user ID the cookie authenticated as.
response.auth_tokenstringThe session token held in the cookie — the same credential the cookie carries, not a separately issued one.
response.expires_inintegerSeconds of remaining life on the token. Always read this rather than assuming a fixed duration; the lifetime can vary. 0 (or a 401 on a later call) means the session is finished and the user must sign in again.

Error Responses

Error CodeHTTP StatusMessageCause
100966401"This endpoint requires a browser session cookie."The request authenticated with an Authorization header, or carried no session cookie at all

Notes


POST /current/user/auth/sign-out/

Invalidate every revocable JWT issued to the calling user. The user's session counter is incremented; revocable tokens carrying the previous counter value will be rejected on their next request.

Auth: Required (JWT, scope: user or admin; or an unscoped API key for the user)

This is account-wide, not session-scoped. It bumps the user's SHARED session counter, so it signs out every revocable session on the account — not only the caller's. An entity-scoped API key is therefore refused with 10175, the same bar invalidate-all/ applies: a key carries no sv claim, so a sign-out never affects the key itself, which makes this purely an action on other people's sessions when called with one. An unscoped key is unaffected. To end only your own session, discard your own credential — this endpoint cannot do that selectively.

Rate Limited: Yes (per user)

Request Example

curl -X POST "https://api.fast.io/current/user/auth/sign-out/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true
}

Notes


POST /current/user/auth/invalidate-all/

Invalidate EVERY login session for the calling user — a strict superset of sign-out. Kills interactive logins (browser, 2FA, password-reset, SSO) regardless of whether they opted into revocable. Any token issued before this call is rejected on its next request.

Use this for "sign me out of all devices" or a suspected account compromise — i.e. account-security actions, as opposed to a per-browser logout button (use sign-out for that). A password change or reset already invalidates other sessions on its own, so calling this afterwards is unnecessary and will additionally invalidate the replacement auth_token that the password change just returned, signing you out of the session you are using. Call it after a password change only when you deliberately want every session gone, including your own.

Auth: Required (a browser login session, or a credential explicitly holding userdetails:*:rw — see the gate below)

This is a strict superset of sign-out/, not a different class of action — both bump the user's shared session counters and both reach every session on the account; this one additionally kills tokens carrying gsv.

This is an account-settings operation and requires userdetails:*:rw. A credential that does not explicitly hold that scope is refused with 403 and 10769 (error.params.reason: userdetails_scope_required). user:*:rw, user:*:rwa, entity-scoped keys and legacy (unscoped) keys are all refused; a browser login session passes. The limited twofactor-scope token from a half-completed 2FA sign-in is refused separately with 10175. POST /current/user/auth/sign-out/ is not gated this way.

Request Example

curl -X POST "https://api.fast.io/current/user/auth/invalidate-all/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true
}

Notes


GET /current/user/auth/check/

Validate current JWT and get user ID.

Auth: Required (Bearer token)

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "id": "1234567890123456789"
}

Response Fields

FieldTypeDescription
response.idstringThe 19-digit numeric user ID.

Notes


GET /current/auth/scopes/

Token scope introspection. Returns information about the current token's scope, auth type, and agent status.

Auth: Required (Bearer token)

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "auth_type": "jwt_v2",
  "scopes": ["org:12345:rw", "org:67890:rw"],
  "scopes_detail": [],
  "is_agent": true,
  "agent_name": "My MCP Agent",
  "full_access": false,
  "admin": false,
  "legacy": false
}

Response Fields

FieldTypeDescription
response.auth_typestringToken type: "jwt_v2" (scoped JWT), "jwt_v1" (legacy JWT, which is what a browser login session is), "api_key" (legacy API key that declares no scopes claim), or "api_key_scoped" (API key with scopes). An OAuth grant for scope=user is now stored explicitly as ["user:*:rw"] and reports jwt_v2 — do not branch on auth_type === "jwt_v1" to detect it.
response.scopesarrayArray of scope strings in entity_type:entity_id:access_mode format. Empty only for a legacy credential that declares no scopes claim at all — a v1 JWT (browser login session) or a pre-scopes API key.
response.scopes_detailarrayHydrated scope details with entity information. Empty when scopes are empty. Each entry carries entity_type, entity_id, access_mode, a boolean admin (true when that entry's access mode is rwa, so a client need not re-parse the string) and the label/name fields described in the OAuth 2.0 reference.
response.is_agentbooleanWhether the token represents an agent.
response.agent_namestring or nullAgent display name. null if not set or not an agent.
response.full_accessbooleanWhether the credential is account-wide and may writeuser:*:rw or user:*:rwa. true for legacy (unscoped) API keys and browser login sessions; false for user:*:r and for entity-scoped credentials. It is not an administration flag — read admin for that.
response.adminbooleanWhether the credential can perform administrative operations. true for a browser login session and for any credential holding an rwa scope. Always present.
response.legacybooleanWhether the credential declares no scopes claim at all — a pre-scopes API key, a pre-scopes OAuth session, and every browser login session. Always present. legacy never implies admin, and admin never implies legacy.

How the flags combine

Credentialfull_accessadminlegacyauth_type
user:*:rwatruetruefalsejwt_v2 / api_key_scoped
user:*:rwtruefalsefalsejwt_v2 / api_key_scoped
user:*:rfalsefalsefalsejwt_v2 / api_key_scoped
Scoped, e.g. org:123:rwafalsetruefalsejwt_v2 / api_key_scoped
Legacy key with no scopestruefalsetrueapi_key
Browser login sessiontruetruetruejwt_v1

Error Response (401 Unauthorized) — the token could not be verified

{
  "result": false,
  "error": {
    "code": 154843,
    "text": "The supplied token could not be verified.",
    "params": { "reason": "verification_failed" }
  }
}

A bearer token that cannot be decoded returns 401 with error.params.reason set to verification_failed. This is never answered with a 200. Treat it as "this token was not checked", NOT as "this token has no rights" — a successful response always describes a token that was read; it never reports the absence of scopes because verification failed.

reason carries a single value here on purpose. The underlying cause — an expired credential, a forged or malformed one, or key material being temporarily unavailable — is not distinguishable at this layer, and reporting a guessed cause would be worse than reporting that it is unknown. Branch on reason, never on error.code (the numeric code identifies the call site and is not a stable contract).


API Keys

POST /current/user/auth/key/

Create a new API key.

Auth: Required (JWT, scope: user or admin)

Request Parameters

ParameterTypeRequiredDescription
memo string No Label/description for the key.
scopes string No JSON array of scope strings (e.g., ["org:123:rw", "workspace:456:r"]). Omitted, empty, or the string "null" stores the explicit ["user:*:rw"] — whole-account read and write, no administration and no account settings. An explicit empty array ([]) is refused with 406 190363; it is not a way to create an unconstrained key.
agent_name string No Agent or application name for tracking. Max 128 characters.
expires string No Expiration datetime. Accepts any strtotime-compatible value; canonical form is Y-m-d H:i:s UTC (e.g. 2026-12-31 23:59:59 UTC). Must be in the future. Omit or null for no expiration.

scopes handling

The same rules apply to creating a key and to updating one (POST /current/user/auth/key/{key_id}/):

SubmittedCreateUpdate
OmittedStored as ["user:*:rw"]Left as-is
"" or "null"Stored as ["user:*:rw"]Clears to ["user:*:rw"]
"[]"Refused — 406 190363Refused — 406 190363
Malformed JSON, a JSON object, a non-string member, or an invalid scope string406 197558406 121158
A non-empty JSON list of valid scope stringsIssuance checks belowIssuance checks below

An unscoped key is never created any more. The explicit ["user:*:rw"] is stored instead, which is the same authority a legacy key has: whole-account read and write, no administration, no account settings.

Issuance checks (create and update)

They run in this order on the final effective scope set:

  1. Empty set406 190363 — "The scopes provided must contain at least one scope."
  2. Broader than the calling credential403 10768, error.params.reason scope_exceeds_issuer — "The requested scopes are broader than the credential making this request." A browser login session skips this check; it is unbounded.
  3. Not grantable to this user406 185003 — the human does not hold the entity, or the scope string is not issuable at all (for example userdetails:*:rwa or fileshare:*:rw).

Request Example

curl -X POST "https://api.fast.io/current/user/auth/key/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "memo=CI/CD Pipeline Key"

Request Example (scoped key with expiration)

curl -X POST "https://api.fast.io/current/user/auth/key/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d "memo=Workspace Agent" \
  -d 'scopes=["workspace:1234567890123456789:rw"]' \
  -d "agent_name=my-agent" \
  -d "expires=2026-12-31 23:59:59 UTC"

Success Response (200 OK)

{
  "result": true,
  "api_key": "{the raw key, shown only here}",
  "key": {
    "id": "{key_id}",
    "memo": "Workspace Agent",
    "scopes": "[\"workspace:1234567890123456789:rw\"]",
    "agent_name": "my-agent",
    "created": "2026-09-09 14:03:11 UTC",
    "expires": "2026-12-31 23:59:59 UTC",
    "admin": false,
    "legacy": false,
    "api_key": "****************************abcd"
  }
}

Response Fields

api_key is the secret. key is the created row, in the same shape the read and update calls return — including its own masked api_key. The two are siblings so that reading the secret stays exactly as simple as it was.

FieldTypeDescription
response.api_keystringThe raw key. Only shown here, only once — it is never retrievable again, so store it before you discard the response.
response.keyobjectThe created key. Carries id, memo, scopes, agent_name, created, expires, admin, legacy and a masked api_key.
response.key.idstringThe key id, for the update, read and delete calls.
response.key.scopesstringThe stored scopes claim, as a JSON list string.
response.key.adminbooleanWhether the key's scopes confer administration. A user:*:rw key is not admin.
response.key.legacybooleanWhether the key predates scoped keys. legacy never implies admin.

key is a new field and the addition is backwards compatible: api_key is unchanged and still the raw string. Use key when you need the new key's id, created or expires without a second call.

key is best effort. It is built by reading the stored row back, and on the rare occasion that read fails the response is {"result": true, "api_key": "…"} with no key field — the key is created and the secret is still returned, because losing the one-time secret to a failed convenience lookup would be the worse outcome. Treat key as optional. If it is absent and you need the row, list your keys with GET /current/user/auth/keys/ and match on created — the degraded response carries the secret only, so there is no id in it to read back by.

Error Responses

Error CodeHTTP StatusMessageCause
10011401"Your credentials were not supplied or invalid."Missing or invalid JWT
10175403"The scope of your credentials are not sufficient."JWT scope not user or admin
10015429"You are at the maximum number of API keys, {max}."Maximum key limit reached
10016406"You provided an invalid Memo."Invalid memo format
190363406"The scopes provided must contain at least one scope."scopes was an explicit empty array ([])
197558406invalid scopes valueMalformed JSON, a JSON object, a non-string member, or an invalid scope string
10770403"Your credential is read-only and is not authorized to make changes."The calling credential holds no write-capable scope anywhere — user:*:r, or a set every entry of which is :r. error.params.reason is scope_write_required
10768403"The requested scopes are broader than the credential making this request."The requested scopes exceed what the calling credential holds. error.params.reason is scope_exceeds_issuer
185003406scopes not grantableA scope the human does not hold, or one that is not issuable at all (e.g. userdetails:*:rwa, fileshare:*:rw)

Notes


GET /current/user/auth/key/{key_id}/

Get details of an API key (key value is masked).

Auth: Required (JWT, scope: user or admin)

Scope visibility. A write-capable account-wide credential (user:*:rw or user:*:rwa), a legacy key, and a signed-in web session see every key on the account. Every other credential — one created with entity scopes, and also user:*:r — sees only the keys its own grant contains — the same containment rule POST /current/user/auth/key/ applies to a mint. A key outside that grant is not visible and answers 403 10175, exactly as before. This is what lets a scoped agent inspect and revoke the keys it creates; it never widens what a credential can reach.

Path Parameters

ParameterTypeRequiredDescription
{key_id} string Yes The API key's unique identifier.

Request Example

curl -X GET "https://api.fast.io/current/user/auth/key/key_12345/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true,
  "api_key": {
    "id": "key_12345",
    "api_key": "****************************ab12",
    "memo": "CI/CD Pipeline Key",
    "created": "2024-01-15 10:30:00 UTC",
    "scopes": "[\"workspace:1234567890123456789:rw\"]",
    "agent_name": "my-agent",
    "expires": "2026-12-31 23:59:59 UTC",
    "admin": false,
    "legacy": false
  }
}

Response Fields

FieldTypeDescription
response.api_key.idstringUnique key identifier.
response.api_key.api_keystringMasked API key (only last 4 characters visible).
response.api_key.memostringKey description/label.
response.api_key.createdstringKey creation timestamp in UTC.
response.api_key.scopesstring or nullJSON array of scope strings. null only on a legacy key that declares no scopes claim at all — newly created keys always store an explicit list, ["user:*:rw"] when none was supplied.
response.api_key.agent_namestring or nullAgent/application name, or null if not set.
response.api_key.expiresstring or nullExpiration datetime in canonical Y-m-d H:i:s UTC format, or null for no expiration.
response.api_key.adminbooleanWhether the key carries at least one rwa scope, i.e. whether it can perform administrative operations (still capped by the human's live role).
response.api_key.legacybooleanWhether the key declares no scopes claim at all. A legacy key behaves as user:*:rw: whole-account read and write, no administration, no account settings. legacy never implies admin, and admin never implies legacy.

Error Responses

Error CodeHTTP StatusMessageCause
10019406"You provided an invalid Token to get details of."Invalid key ID format
10175403"The scope of your credentials are not sufficient."The key exists and is yours, but its scopes are not contained in your credential's grant — see Scope visibility above
(none)404no error body — result: false onlyKey does not exist at all — no error.code is returned in this case; a key that exists but belongs to a different user instead returns error.code 199646 with the same "The API Key was not found." message

POST /current/user/auth/key/{key_id}/

Update an existing API key's memo, scopes, agent_name, and/or expires.

Auth: Required (JWT, scope: user or admin)

The update verb is POST. There is no PUT on this path.

Containment is re-checked on every update, including a metadata-only edit such as changing expires or memo: the key's stored scopes are the effective set being re-authorized, so they must still be covered by the credential making the request. A request that would leave the key broader than its issuer is refused with 403 and 10768 (error.params.reason: scope_exceeds_issuer). A browser login session is unbounded and skips that check.

Path Parameters

ParameterTypeRequiredDescription
{key_id} string Yes The API key's unique identifier.

Request Parameters

ParameterTypeRequiredDescription
memo string No Updated label/description for the key.
scopes string No JSON array of scope strings. Omit to leave the stored scopes unchanged. Send an empty string or "null" to clear them to the explicit ["user:*:rw"] — whole-account read and write, no administration and no account settings. An explicit empty array ([]) is refused with 406 190363. See scopes handling and the issuance checks above.
agent_name string No Agent/application name. Send empty string or "null" to clear. Max 128 characters.
expires string No Expiration datetime. Accepts any strtotime-compatible value; canonical form is Y-m-d H:i:s UTC (e.g. 2026-12-31 23:59:59 UTC). Must be in the future. Send empty string or "null" to clear (no expiration).

Request Example

curl -X POST "https://api.fast.io/current/user/auth/key/key_12345/" \
  -H "Authorization: Bearer {jwt_token}" \
  -d 'scopes=["org:1234567890123456789:r"]' \
  -d "agent_name=updated-agent"

Success Response (200 OK)

{
  "result": true,
  "api_key": {
    "id": "key_12345",
    "api_key": "****************************ab12",
    "memo": "CI/CD Pipeline Key",
    "created": "2024-01-15 10:30:00 UTC",
    "scopes": "[\"org:1234567890123456789:r\"]",
    "agent_name": "updated-agent",
    "expires": null,
    "admin": false,
    "legacy": false
  }
}

Error Responses

Error CodeHTTP StatusMessageCause
187851 or 100527404"The API Key was not found."187851 when the key does not exist at all; 100527 when it exists but belongs to another user
121158 / 107184 / 163622406Various121158 invalid scopes JSON (malformed, an object, a non-string member, or an invalid scope string); 107184 invalid agent_name; 163622 invalid/past expires
190363406"The scopes provided must contain at least one scope."scopes was an explicit empty array ([])
10770403"Your credential is read-only and is not authorized to make changes."The calling credential holds no write-capable scope anywhere — user:*:r, or a set every entry of which is :r. error.params.reason is scope_write_required
10768403"The requested scopes are broader than the credential making this request."The key's effective scopes exceed what the calling credential holds — re-checked on every update, including metadata-only edits. error.params.reason is scope_exceeds_issuer
185003406scopes not grantableA scope the human does not hold, or one that is not issuable at all (e.g. userdetails:*:rwa, fileshare:*:rw)

Notes


DELETE /current/user/auth/key/{key_id}/

Delete an API key.

Auth: Required (JWT, scope: user or admin)

Scope visibility. A write-capable account-wide credential (user:*:rw or user:*:rwa), a legacy key, and a signed-in web session see every key on the account. Every other credential — one created with entity scopes, and also user:*:r — sees only the keys its own grant contains — the same containment rule POST /current/user/auth/key/ applies to a mint. A key outside that grant is not visible and answers 403 10175, exactly as before. This is what lets a scoped agent inspect and revoke the keys it creates; it never widens what a credential can reach.

Path Parameters

ParameterTypeRequiredDescription
{key_id} string Yes The API key's unique identifier.

Request Example

curl -X DELETE "https://api.fast.io/current/user/auth/key/key_12345/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
10019406"You provided an invalid Token to Delete."Invalid key ID format
10020404"You provided a Token that was not found."Key not found or belongs to another user
10021500"There was an error deleting the API Key."Internal deletion failure
10175403"The scope of your credentials are not sufficient."The key exists and is yours, but its scopes are not contained in your credential's grant — see Scope visibility above

Notes


GET /current/user/auth/keys/

List all API keys for the user.

Auth: Required (JWT, scope: user or admin)

Scope visibility. A write-capable account-wide credential (user:*:rw or user:*:rwa), a legacy key and a signed-in web session list every key on the account. Every other credential — one created with entity scopes, and also user:*:r — lists only the keys its own grant contains — the same containment rule POST /current/user/auth/key/ applies to a mint. Keys outside that grant are omitted from api_keys and are not counted in results; a narrowed credential that owns no such key receives results: 0 and api_keys: null, the same answer an account with no keys receives.

This is what lets a scoped agent enumerate and revoke the keys it creates. It never widens what a credential can reach: a key it could not have minted stays invisible, and a credential whose scopes cannot be read at all is still refused outright with 403 10175.

Request Example

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

Success Response (200 OK)

{
  "result": true,
  "results": 2,
  "api_keys": [
    {
      "id": "key_12345",
      "api_key": "****************************ab12",
      "memo": "CI/CD Pipeline Key",
      "created": "2024-01-15 10:30:00 UTC",
      "scopes": "[\"workspace:1234567890123456789:rw\"]",
      "agent_name": "my-agent",
      "expires": "2026-12-31 23:59:59 UTC",
      "admin": false,
      "legacy": false
    },
    {
      "id": "key_67890",
      "api_key": "****************************cd34",
      "memo": "Backup Script",
      "created": "2024-02-20 14:00:00 UTC",
      "scopes": null,
      "agent_name": null,
      "expires": null,
      "admin": false,
      "legacy": true
    }
  ]
}

No Keys Response (200 OK)

{
  "result": true,
  "results": 0,
  "api_keys": null
}

Response Fields

FieldTypeDescription
response.resultsintegerNumber of API keys.
response.api_keysarray or nullArray of API key objects, or null if none exist.
response.api_keys[].idstringUnique key identifier.
response.api_keys[].api_keystringMasked API key (only last 4 characters visible).
response.api_keys[].memostringKey description/label.
response.api_keys[].createdstringKey creation timestamp in UTC.
response.api_keys[].scopesstring or nullJSON array of scope strings. null only on a legacy key that declares no scopes claim at all.
response.api_keys[].agent_namestring or nullAgent/application name, or null if not set.
response.api_keys[].expiresstring or nullExpiration datetime in canonical Y-m-d H:i:s UTC format, or null for no expiration.
response.api_keys[].adminbooleanWhether the key carries at least one rwa scope.
response.api_keys[].legacybooleanWhether the key declares no scopes claim at all (it then behaves as user:*:rw).

Two-Factor Authentication (2FA)

GET /current/user/auth/2factor/

Get current 2FA status.

Auth: Required (JWT, scope: user or admin)

Request Example

curl -X GET "https://api.fast.io/current/user/auth/2factor/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true,
  "state": "enabled",
  "totp": false
}

Response Fields

FieldTypeDescription
response.statestring2FA status: "enabled" (fully verified), "unverified" (added but not verified), or "disabled" (not configured).
response.totpbooleanWhether the 2FA method is TOTP (Time-based One-Time Password).

POST /current/user/auth/2factor/{channel}/

Enable 2FA on the account.

Auth: Required (JWT, scope: user or admin)

Enrolling in 2FA is an account-settings operation and requires userdetails:*:rw. A credential that does not explicitly hold that scope is refused with 403 and 10769 (error.params.reason: userdetails_scope_required) — user:*:rw, user:*:rwa, entity-scoped keys and legacy (unscoped) keys alike. A browser login session passes. The 2FA login challenge endpoints are not gated this way.

Path Parameters

ParameterTypeRequiredDefaultDescription
{channel} string No sms 2FA delivery channel: sms, call, whatsapp, or totp.

Request Example

curl -X POST "https://api.fast.io/current/user/auth/2factor/sms/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response for SMS/Voice/WhatsApp (202 Accepted)

{
  "result": true
}

Success Response for TOTP (202 Accepted)

{
  "result": true,
  "binding_uri": "otpauth://totp/fast.io:jane@example.com?secret=ABCDEF..."
}

Response Fields (TOTP only)

FieldTypeDescription
response.binding_uristringTOTP provisioning URI for QR code display.

Error Responses

Error CodeHTTP StatusMessageCause
10167409"2Factor already added, please remove first."2FA already enabled
10173406"An invalid channel was supplied."Invalid channel name
10168406"2Factor cannot be added, you need a valid phone_number and phone_country..."No phone number configured

Notes


POST /current/user/auth/2factor/verify/{token}/

Verify a 2FA setup code to confirm enrollment. Transitions 2FA from unverified to enabled state.

Auth: Required (JWT, scope: user or admin)

This is the enrollment step, performed from a fully signed-in session, so the limited twofactor-scope token issued during a 2FA sign-in is refused here with 10175 — that token belongs to POST /current/user/auth/2factor/auth/{token}/, which completes a login rather than confirming enrollment.

Verifying 2FA enrolment is an account-settings operation and requires userdetails:*:rw. A credential that does not explicitly hold that scope is refused with 403 and 10769 (error.params.reason: userdetails_scope_required) — user:*:rw, user:*:rwa, entity-scoped keys and legacy (unscoped) keys alike. A browser login session passes.

Path Parameters

ParameterTypeRequiredDescription
{token} string Yes 2FA verification code (e.g., 6-digit code).

Request Example

curl -X POST "https://api.fast.io/current/user/auth/2factor/verify/123456/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (202 Accepted)

{
  "result": true
}

Verification Failed (406 Not Accepted)

{
  "result": false
}

Error Responses

Error CodeHTTP StatusMessageCause
10173406"An invalid token was supplied to validate."Invalid token format
10170406"2Factor is not enabled."2FA not configured

Notes


POST /current/user/auth/2factor/auth/{token}/

Authenticate with a 2FA code. Upgrades a limited-scope JWT to a full-scope JWT.

Auth: Required (JWT, scope: user, twofactor, or admin)

Path Parameters

ParameterTypeRequiredDescription
{token} string Yes Valid 2FA verification code (e.g., 6-digit TOTP or SMS code).

Request Headers

HeaderTypeRequiredDefaultDescription
x-ve-session-cookie boolean No (absent) Browser clients only. Same meaning and vocabulary (1, true, yes) as on GET /current/user/auth/: the full-scope token minted here is ALSO delivered in an HttpOnly, Secure, SameSite=Lax cookie scoped to the site's registrable domain (e.g. fast.io). Only honoured as a request header.

Request Example

curl -X POST "https://api.fast.io/current/user/auth/2factor/auth/123456/" \
  -H "Authorization: Bearer {twofactor_jwt_token}"

A browser client that opted into the session cookie at sign-in sends the same header again here:

curl -X POST "https://api.fast.io/current/user/auth/2factor/auth/123456/" \
  -H "Authorization: Bearer {twofactor_jwt_token}" \
  -H "x-ve-session-cookie: 1" \
  --cookie-jar cookies.txt

Success Response (200 OK)

{
  "result": true,
  "expires_in": 86400,
  "auth_token": "{jwt_token}"
}

Response Fields

FieldTypeDescription
response.expires_inintegerJWT expiration time in seconds.
response.auth_tokenstringNew JWT with full user scope.

Error Responses

Error CodeHTTP StatusMessageCause
10173406"An invalid token was supplied to authenticate."Invalid token format
10172406"2Factor is not enabled on this account."2FA not enabled
10174406"The supplied token failed to authenticate."Wrong 2FA code
10009401"Internal Error."JWT creation failure

Notes


DELETE /current/user/auth/2factor/{token}/

Disable (remove) 2FA from the account.

Auth: Required (JWT, scope: user or admin)

Path Parameters

ParameterTypeRequiredDescription
{token} string Yes Valid 2FA verification code. Required only if 2FA is in enabled (verified) state.

Request Example

curl -X DELETE "https://api.fast.io/current/user/auth/2factor/123456/" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusMessageCause
10173406"An invalid token was supplied, valid token required to remove 2Factor."Invalid token format
10174406"The supplied token failed to authenticate."Token verification failed
10169500"2Factor could not be removed, please contact support."Internal removal failure

Notes


2FA Code Delivery Endpoints

Request a 2FA code via different channels. All require auth (accepts user, twofactor, or admin JWT scope).

GET /current/user/auth/2factor/send/sms/

Send code via SMS

GET /current/user/auth/2factor/send/call/

Send code via voice call

GET /current/user/auth/2factor/send/whatsapp/

Send code via WhatsApp

Success Response (202 Accepted)

{
  "result": true
}

Failure Response (406 Not Accepted)

{
  "result": false
}

Error Responses

Error CodeHTTP StatusMessageCause
10011401"Your credentials were not supplied or invalid."Invalid JWT
10175403"The scope of your credentials are not sufficient."Wrong JWT scope
10170406"2Factor is not enabled."2FA not configured on account

Notes


Complete 2FA Flows

Complete 2FA Login Flow

1. GET /current/user/auth/
   - Send email:password via HTTP Basic Auth
   - Response includes "2factor": true and limited-scope auth_token

2. GET /current/user/auth/2factor/send/sms/  (or /call/ or /whatsapp/)
   - Request a fresh 2FA code
   - Uses the limited-scope (twofactor) JWT

3. POST /current/user/auth/2factor/auth/{code}/
   - Submit the 2FA code
   - Receive a new JWT with full "user" scope
   - Use this token for all subsequent requests

Complete 2FA Setup Flow

1. POST /current/user/auth/2factor/{channel}/
   - Choose channel: sms, call, whatsapp, or totp
   - Phone number must be configured on account (for non-TOTP)
   - State becomes "unverified"
   - For TOTP: receive binding_uri for QR code

2. Receive code via selected channel (or scan QR code for TOTP)

3. POST /current/user/auth/2factor/verify/{code}/
   - Submit the verification code
   - State becomes "enabled"
   - 2FA is now active on the account

Complete 2FA Removal Flow

1. DELETE /current/user/auth/2factor/{code}/
   - Must provide valid 2FA code if state is "enabled"
   - Can remove without code if state is "unverified"
   - 2FA is fully removed from the account

Search for people by name or email across your contacts and the people you share access with (members of orgs, workspaces, and shares you belong to).

Auth: Required (JWT)

Query Parameters

ParameterTypeRequiredDescription
search string Yes Search term. Matches against user names and email addresses. Must not be blank.

Request Example

curl -X GET "https://api.fast.io/current/users/search/?search=john" \
  -H "Authorization: Bearer {jwt_token}"

Success Response (200 OK)

{
  "result": true,
  "contacts": {
    "john.doe@example.com": "John Doe",
    "jane.johnson@example.com": "Jane Johnson"
  },
  "users": [
    {
      "id": "1234567890123456789",
      "email": "john.doe@example.com",
      "name": "John Doe"
    },
    {
      "id": null,
      "email": "jane.johnson@example.com",
      "name": "Jane Johnson"
    }
  ]
}

Response Fields

FieldTypeDescription
response.contactsobjectMap of email address (key) to display name (value) for each matched user. Unchanged, backward-compatible.
response.usersarrayList of matched people as {id, email, name} objects, deduplicated by email. Provides the user id the contacts map cannot.
response.users[].idstring | nullThe matched person's user id when they are reachable through one of your shared spaces (an org / workspace / share you have in common); null for a contacts-only match that does not resolve to such an account.
response.users[].emailstringThe matched person's email address.
response.users[].namestringThe matched person's display name.

Error Responses

Error CodeHTTP StatusMessageCause
10011401"Authentication required"Missing or invalid JWT token
205516 / 207092406"This value should not be blank."No custom error code is attached to this field — the code is derived from Symfony's validator: 205516 when search is omitted entirely, 207092 when present but blank
157360 or 120189500"Internal error"157360 when the contacts search client fails to initialize; 120189 when the user-profile search client fails to initialize

Notes

Response Envelope

Success

{"result": true, ...}

Error

{
  "result": false,
  "error": {
    "code": 195654,
    "text": "Human-readable message",
    "documentation_url": "https://api.fast.io/llms.txt",
    "resource": "POST /current/user/"
  }
}

Validation error (HTTP 406) with structured per-parameter detail

{
  "result": false,
  "error": {
    "code": 195654,
    "text": "The email parameter is required. The password must be at least 8 characters.",
    "documentation_url": "https://api.fast.io/llms.txt",
    "resource": "POST /current/user/",
    "params": [
      {"name": "email", "kind": "missing", "message": "The email parameter is required.", "code": 195654},
      {"name": "password", "kind": "invalid", "message": "The password must be at least 8 characters.", "code": 195655, "expected_type": "string"}
    ]
  }
}

error.params is an array of {name, kind, message, code, expected_type?, received_alias?}. It is present on validation errors (HTTP 406) and aggregates every failed parameter so callers see all problems in one round trip. kind is one of missing, invalid, type_mismatch, or unknown_parameter on a validation error, and conflict on a state conflict (HTTP 409). Treat kind as open-ended: handle an unrecognised value as a generic failure rather than rejecting the response. The field is omitted when empty (non-validation errors). The existing text field is retained byte-identically for compatibility and is now advisory — clients should prefer params for programmatic handling.

OPTIONS introspection. Most user/auth endpoints respond to OPTIONS with a JSON description of their accepted parameters (source, required vs optional, expected type, declared constraints). Use this to fetch parameter requirements before issuing a call. Endpoints that don't opt in return 405 Method Not Allowed.

Common Error Codes

CodeDescriptionHTTP Status
1600Internal Error500 Internal Server Error
1605Invalid Input406 Not Acceptable
1658Not Acceptable406 Not Acceptable
1607Duplicate Entry406 Not Acceptable
1669Already Exists409 Conflict
1660Conflict409 Conflict
1609Not Found / Resource Missing404 Not Found
1610General Error500 Internal Server Error
1650Authentication Invalid401 Unauthorized
1651Invalid Request Type405 Method Not Allowed
1653User Not Found404 Not Found
1701Gone410 Gone — endpoint retired by decision; stop calling the path, do not retry or vary the id
1671Rate Limited429 Too Many Requests
1680Access Denied401 Unauthorized
1670Restricted406 Not Acceptable
1677Locked423 Locked
1673SSO Auth Error401 Unauthorized

Scope and access-mode refusals

Three codes report that a credential was understood but is too narrow for what was asked. All three are HTTP 403, never 401 — the credential is valid, so re-authenticating or refreshing will not help; the credential has to be re-issued with wider scopes from a signed-in web session.

Codeparams.reasonWhen
10767scope_admin_requiredAn administrative operation called with a credential that is not admin-capable
10768scope_exceeds_issuerThe requested scopes are broader than the credential making the request (API-key create/update, OAuth session narrowing)
10768access_mode_exceeds_initiateAn OAuth consent asked for a broader access mode, or for account settings, than the authorization was initiated with
10769userdetails_scope_requiredAn account-settings operation without userdetails:*:rw
10770scope_write_requiredA non-GET method on an account-anchored route, called with a credential that holds no write-capable grant anywhere — user:*:r, or a set every entry of which is :r

On these four codes error.params is an object (a map), not the per-parameter array returned by validation errors:

FieldAlways presentValue
reasonYesOne of the four strings above
entity_typeYese.g. org, workspace, share, user, userdetails
entity_idYesThe entity id as a string, or null for an account-wide refusal. It is quoted because profile ids are 19 digits and exceed the range a JSON number survives in a client that parses numbers as doubles
required_access_modeYese.g. rwa, rw
current_access_modeYesWhat the credential holds for that entity, or null
credential_typeYesapi_key, oauth or session
credential_idNoPresent for an API-key caller
credential_labelNoPresent when the key has an agent name

On the consent-time 10768 (access_mode_exceeds_initiate) refusals, current_access_mode instead reports the access mode the consent requested, and required_access_mode reports the ceiling the authorization was initiated with.

Branch on error.params.reason, not on the numeric code. Existing entity-permission codes (10545, 10560, 10574, 10753, 10754, 10757, 10175) are unchanged.

Rate Limiting

Response headers: x-ve-limit-avail (requests remaining), x-ve-limit-max (window cap), x-ve-limit-expires (Unix-time the window resets).

When exceeded: HTTP 429 with error code 1671 (Rate Limited). Back off until x-ve-limit-expires.

ID Formats

Token Types

TypeFormatLifetimeUse
JWT (Basic Auth)RS256-signed JSON Web TokenConfigurable (default varies)General API access
JWT (OAuth)RS256-signed JSON Web Token1 hourOAuth-based API access
Refresh TokenOpaque stringLong-livedObtaining new access tokens (OAuth only)
API KeyAlphanumeric stringConfigurable (default: no expiry)Service-to-service communication. Optionally scoped with permissions, agent name, and expiration.

Security Best Practices

  1. Always use HTTPS for all API communication.
  2. Store refresh tokens and API keys securely (OS keychain, encrypted storage).
  3. Never log tokens in client-side logs or analytics.
  4. Persist the refresh_token from the response; it is long-lived and returned unchanged on refresh (no rotation needed).
  5. Verify the state parameter in OAuth callbacks to prevent CSRF.
  6. Handle 401 responses by attempting a token refresh; if refresh fails, re-authenticate.
  7. Revoke tokens on logout by calling the revoke endpoint and clearing local storage.
↑ Back to top