Events & Activity Event search, activity polling, WebSocket realtime.

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

Events capture every action in the system — file operations, membership changes, comments, AI activity, billing, and more. Use the event search endpoints to query the log, activity polling for efficient change detection, and WebSocket for real-time delivery.

Search and filter the event log. Events capture every action in the system — file operations, membership changes, comments, AI activity, billing, and more.

Search Events

GET /current/events/search/

Search and filter events with comprehensive filtering options. Pages by offset or by an opaque keyset cursor.

Auth: Required (JWT). Subject to global rate limiting; see the rate-limit section in the main reference.

Query Parameters

ParameterTypeRequiredDefaultDescription
user_id string Conditional — Filter by user profile ID. One of user_id, org_id, workspace_id, share_id, or parent_event_id is required.
org_id string Conditional — Filter by organization profile ID
workspace_id string Conditional — Filter by workspace profile ID
share_id string Conditional — Filter by share profile ID
parent_event_id string Conditional — Filter by parent event ID for serial/batch events. Cannot combine with filters other than acknowledged, visibility, limit, offset.
event string No — Filter by specific event name (e.g., workspace_storage_file_added). Max 100 characters.
category string No — Filter by event category. See Event Categories.
subcategory string No — Filter by event subcategory. See Event Subcategories.
calling_user_id string No — Filter by the user who triggered the event (19-digit numeric ID)
object_id string No — Filter by related object ID (file, folder, etc.)
acknowledged string No — Filter by acknowledgment status: "true" or "false"
visibility string No All non-internal "external_audit_log" or "external"
created-min string No — Lower bound for event creation time. Accepts ISO 8601 (e.g., 2025-12-01T06:00:00Z) or YYYY-MM-DD HH:MM:SS
created-max string No — Upper bound for event creation time. Same format as created-min; must not be earlier than created-min
limit integer No 100 Maximum number of results (1–250)
offset integer No 0 Number of results to skip for pagination. Ignored in cursor mode; sending a non-zero offset together with cursor is rejected.
cursor string No — Keyset cursor for the next page — pass back the pagination.next_cursor you were given. Cannot be combined with a non-zero offset or with parent_event_id. See "Paging a search" below.
output string No — Select the response shape. Comma-separated tokens (e.g. terse, standard, full). See the "Compact Responses" section below for the three detail levels and the fields returned at each level.

Profile filter priority: If multiple profile filters are supplied, priority is: user_id > org_id > workspace_id > share_id. Only the highest-priority filter is applied.

Example Request

curl -X GET "https://api.fast.io/current/events/search/?workspace_id=1234567890123456789&category=workspace&subcategory=transfer&limit=50" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "events": [
    {
      "event_id": "ancou-ywgcx-iff7k-pbijp-l4ysg-n43j",
      "category": "workspace",
      "sub_category": "transfer",
      "visibility": "external",
      "permission": "member",
      "severity": "medium",
      "notification": "affected_user",
      "description": "The file 'quarterly_report.pdf' has been added to the workspace 'Engineering' by 'Jane Smith'.",
      "event": "workspace_storage_file_added",
      "required_params": ["file_added", "workspace"],
      "node_type": "file",
      "operation_type": "add",
      "requires_parent_node": false,
      "event_profile": "1234567890123456789",
      "calling_user": "9876543210987654321",
      "object_id": "2lavr2y6uogubeyvfmqv2ur6k6ezt",
      "template": {
        "description": "The file '{{#name file_added}}' has been added to the workspace '{{#name workspace}}'{{#if calling_user}} by '{{#fullname calling_user}}'{{/if}}.",
        "params": ["2lavr2y6uogubeyvfmqv2ur6k6ezt", "1234567890123456789", "9876543210987654321", "9876543210987654321", null]
      },
      "actor": {
        "user_id": "9876543210987654321",
        "kind": "agent",
        "agent_name": "Dobby",
        "name_source": "api_key_label",
        "credential_type": "api_key",
        "verified": false
      },
      "created": "2025-01-20 10:30:45 UTC",
      "org_id": "1111111111111111111",
      "workspace_id": "1234567890123456789",
      "user_id": "9876543210987654321",
      "acknowledged": false
    }
  ],
  "pagination": {
    "has_more": true,
    "next_cursor": "{opaque_cursor}",
    "page_size": 50
  }
}

Response Fields

FieldTypeDescription
resultbooleantrue on success
eventsarrayArray of event objects
events[].event_idstringUnique event identifier (OpaqueId, emitted in the hyphenated form)
events[].createdstringEvent timestamp (Y-m-d H:i:s UTC)
events[].acknowledgedbooleanWhether the current user has acknowledged this event
events[].eventstringEvent name identifier (e.g., workspace_storage_file_added)
events[].categorystringEvent category name
events[].sub_categorystringEvent subcategory name
events[].object_idstringRelated object OpaqueId (if applicable)
events[].calling_userstring19-digit numeric ID of the attributed user, when one was recorded
events[].actorobjectWho acted: user_id, kind (human, agent, api_key, app, system, unknown), agent_name, name_source, credential_type, verified. Absent on events recorded before attribution existed. See the note below
events[].org_idstringOrganization ID context (if applicable)
events[].workspace_idstringWorkspace ID context (if applicable)
events[].share_idstringShare ID context (if applicable)
events[].user_idstringThe event’s subject user: the user the event names (for example a member added or mentioned), otherwise the owner of the org, workspace, or share the event belongs to. Absent on events recorded without one
pagination.has_morebooleanWhether more matching events exist. The only end-of-data signal
pagination.next_cursorstring | nullPass back as cursor for the next page. null whenever has_more is false, and always null when paging by parent_event_id
pagination.page_sizeintegerThe limit you requested (not the number of events returned)

Rows also carry the event’s definition and rendering fields: visibility, permission, severity, notification, required_params, event_profile (the profile the event was recorded against), the rendered description, and template (the description with {{...}} tokens plus their positional params). File and folder events add node_type, operation_type and requires_parent_node. A few events promote extra fields to the row (for example policy_changes on org_updated, or the fields listed under Compliance & Audit below). Other data supplied when the event was recorded, such as file names or sizes, is not returned as separate fields; names appear only inside description.

Audit-mode-only fields. When visibility=external_audit_log is requested, every row additionally carries ip (string or null) and country (string or null, ISO-3166 alpha-2, or XX/T1) — the client IP and country the event was recorded under. Both are null for historical rows and for events written without a request (background jobs). These two keys are absent in external or default-mode rows, so ordinary members never see a peer’s IP through this endpoint. The sign-in telemetry fields (method, mfa, new_country, client_id, session_id, token_id, agent_name, device_name, user_agent) are audit-mode only in the same way.

Actor attribution. actor is the same object carried by storage nodes, versions, locks, comments and metadata facts (full field reference: Actor Attribution in the Storage reference). It says who the event is credited to and whether an agent acted for them. verified: true appears only on Fastio’s own built-in agent; every other agent_name is self-declared by the credential’s owner or client, so display it but never treat it as verified identity. The older agent_delegation field is legacy — still returned for compatibility, but prefer actor. Events recorded before attribution existed carry no actor; a client may fall back to agent_delegation on those rows. Events from platform editor and pipeline sessions carry no actor, and no longer carry agent_delegation either.

Paging a search

The pagination block is returned on every response, whether you page by offset or by cursor. It is additive — a client that ignores it and keeps stepping offset keeps working unchanged.

Rate limiting: if a search is throttled you get 429, which may carry a Retry-After header giving the number of seconds to wait before retrying — honour it when present. When it's absent, back off using the x-ve-limit-* headers instead. The header is exposed to cross-origin browser clients.

Error Responses

Reading the error tables: the four-digit 16xx/17xx values below are HTTP-status classes, not error.code. The error.code a client actually receives is assigned per endpoint, so use the HTTP status as the gate and a documented error.code — five or six digits, plus the 9661-9669 family — only as a refinement. A 16xx value identifies the status class — useful for telling which kind of failure occurred — but comparing one against error.code will never match. Five- and six-digit codes (and the 9661-9669 family) are real error.code values. If you widen a check from a specific code to a status, widen what you assert with it — a status covers failures the narrower code did not, so a message written for that one code becomes a confident falsehood on the rest.

Error CodeHTTP StatusDescription
1605 (Invalid Input)406No profile filter or parent_event_id provided
1605 (Invalid Input)406created-min is greater than created-max
1605 (Invalid Input)406parent_event_id combined with disallowed filters
1605 (Invalid Input)406cursor combined with a non-zero offset, or with parent_event_id
1605 (Invalid Input)406Invalid pagination cursor — malformed, altered, over-long, or issued for a different user or filter set
1605 (Invalid Input)406Invalid datetime format for created-min or created-max
1605 (Invalid Input)406A profile filter carries an id of a different type (e.g. a workspace id passed as org_id)
1680 (Access Denied)401An audit-log query that includes user_id or is anchored only on parent_event_id (see Notes) — no reason field
1700 (Access Forbidden)403The token’s scope does not include the org_id, workspace_id, or share_id you filtered on — no reason field
1700 (Access Forbidden)403An audit-log query made with a credential that is not admin-capable (rwa) on the anchor — params.reason:"scope_admin_required"
1600 (Query Error)500Internal error during event search
1650 (Authentication Invalid)401Missing or invalid JWT token
1651 (Invalid Request Type)405Wrong HTTP method (only GET accepted)
1700 (Access Forbidden)403An anchored audit-log query's caller is neither admin+ nor an entitled compliance auditor — params.reason:"compliance_access_required"

Notes: Results may be slightly delayed due to caching. OAuth scoped tokens enforce entity-level access: events are filtered to only entities within the token's scope. Events the user cannot access are automatically excluded from results.

Audit-log queries are gated, in two steps. Requesting visibility=external_audit_log first needs an anchor: an org_id, workspace_id, or share_id filter. An audit-log query that includes user_id (even together with org_id/workspace_id/share_id — user_id takes precedence) or that is anchored only on parent_event_id is 1680/401 with no reason field — this is not the compliance gate below; retry with an entity-anchored filter and no user_id. Two shapes fail input validation first (1605/406) and never reach this check: parent_event_id combined with user_id or any other profile filter, and a query with no profile filter and no parent_event_id. Given an anchor, the caller must hold admin permission on that profile, or — only when the anchor is an org_id — hold the org’s compliance_auditor flag (Enterprise only — see Compliance & Audit in the Organizations reference; the auditor flag does not extend to a workspace_id/share_id anchor, only admin permission does there). That second check's refusal is 403 with params.reason:"compliance_access_required" — branch on reason, not the code. An auditor sees every org-owned audit row even for a workspace they are not a member of; legal-hold events are the one exception and stay owner/auditor-only.

user_id is refused in audit mode — any audit-log query that includes user_id, with or without another profile filter, is 1680/401 with no reason (see above; user_id with parent_event_id is the 1605/406 input error instead) — use calling_user_id (the actor) instead. For login history: visibility=external_audit_log&event=user_login&calling_user_id={uid}, not user_id.


Summarize Events (AI)

GET /current/events/search/summarize/

Search events and generate an AI-powered natural language summary. Accepts all parameters from /events/search/ plus user_context.

This endpoint does not accept cursor and returns no pagination block.

It is a rollup, not a pager. events and the summary cover up to limit events you can see, taken from the matching activity starting at offset. Events you cannot see are skipped and the scan continues, but it stops after examining 2,000 matching events, so a short (or empty) events array is not proof that nothing else matched. offset skips matching events before visibility is applied, so stepping offset by limit does not walk the log — to read every event, page GET /current/events/search/ with cursor.

Auth: Required (JWT). Subject to global rate limiting; shares the events search rate limit bucket.

Additional Query Parameters

ParameterTypeRequiredDefaultDescription
user_context string No "" Focus guidance for the AI summary (e.g., "Focus on uploads"). Max 64 chars; letters, numbers, spaces, . , ! ? ' - only.

All other parameters are identical to Search Events.

Example Request

curl -X GET "https://api.fast.io/current/events/search/summarize/?workspace_id=1234567890123456789&user_context=Focus%20on%20file%20uploads&limit=100" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "summary": {
    "text": "@[user:9876543210987654321:Jane Smith] uploaded 12 files...",
    "metrics": {
      "total_events": 50,
      "unique_actors": 5,
      "date_range": {
        "start": "2025-01-01 00:00:00 UTC",
        "end": "2025-01-20 10:30:45 UTC"
      },
      "categories": {
        "workspace": 38,
        "share": 12
      }
    }
  },
  "events": []
}

events carries the same event objects as Search Events (left empty above for brevity).

Response Fields (additional to events search)

FieldTypeDescription
summaryobject|nullAI-generated summary, or null if no events, generation failed, or the billing org’s AI policy denies it (see summary_reason)
summary_reasonstringPresent only when summary is withheld for a known reason. Currently one value: ai_policy_denied — every organization whose events were candidates for the summary denies the caller’s Ripley Agent access. The response is still 200 with the events array populated; only the summary is withheld.
summary.textstringNatural language summary with @[type:ID:name] mention pills
summary.metrics.total_eventsintegerTotal events summarized (at most limit; not a count of everything that matched)
summary.metrics.unique_actorsintegerDistinct users who triggered events
summary.metrics.date_range.startstringEarliest event timestamp (Y-m-d H:i:s UTC)
summary.metrics.date_range.endstringMost recent event timestamp (Y-m-d H:i:s UTC)
summary.metrics.categoriesobjectMap of category names to event counts

Summary Mention Pill Formats

EntityFormatExample
User@[user:USER_ID:Display Name]@[user:9876543210987654321:Jane Smith]
File@[file:FILE_ID:filename.ext]@[file:ancouywgcxiff7kpbijpl4ysgn43j:report.pdf]
Folder@[folder:FOLDER_ID:foldername]@[folder:2lavr2y6uogubeyvfmqv2ur6k6ezt:Projects]
Workspace@[workspace:WS_ID:name]@[workspace:1234567890123456789:Engineering]
Share@[share:SHARE_ID:name]@[share:5555555555555555555:Client Files]

Error Responses

All errors from /events/search/ apply, plus:

Error CodeHTTP StatusDescription
1680 (Access Denied)401The org_id, workspace_id, or share_id filter names a profile you are not a member of
1609 (Not Found)404The org_id, workspace_id, or share_id filter names a profile that does not exist
1605 (Invalid Input)406The id supplied is not of the type the parameter expects (e.g. a workspace id passed as org_id)
(generated per call site)503access_policy_unavailable — an owning org’s Ripley Agent policy verdict could not be read; retry.

Notes: Summary generation failures are non-fatal: summary is null but events are still returned.

A user_id or parent_event_id query can span more than one organization. Each candidate event’s owning org is asked, independently, whether its AI policy allows the caller Ripley Agent access; events belonging to an org that denies it are left out of what the summarizer reads, even though the returned events array itself is unaffected. If every organization in the candidate set refuses, summary comes back null with summary_reason: "ai_policy_denied" — the same shape as the single-org case. An org_id/workspace_id/share_id-scoped query only ever touches one org, so this only matters for the two profile-less anchors.

You must belong to the profile you filter on. This endpoint consumes AI tokens that are billed to the filtered profile's organization, so org_id, workspace_id, and share_id are authorized before any summary is generated: the profile must exist, the id must be of the matching type, and you must hold an active membership on it. /events/search/ (the same query without the summary) is unchanged.

The account billed is the one you filtered on. The summary is charged to the organization that owns the profile the query is scoped to — the same profile that selects which events are searched. A query scoped to yourself (user_id, or a parent_event_id query) is charged to your own billing account. Where more than one filter is supplied, the one that selects the events (in the order user_id, org_id, workspace_id, share_id) is the one authorized and billed; the others do not select or bill anything, but they are still validated, so a malformed or wrong-type value in any of them fails the request. user_id, org_id, workspace_id and share_id are type-checked here, as on Search Events: each must carry an id of the kind the parameter names. If the billing organization cannot be resolved, the request still succeeds and returns the events with summary set to null; it is never charged to a different account.

The same two-step audit-log gate as /events/search/ applies (see its Notes): an audit-log query that includes user_id (even together with org_id/workspace_id/share_id — user_id takes precedence) or that is anchored only on parent_event_id is 1680/401 with no reason; parent_event_id combined with user_id or any other profile filter, and a query with no profile filter and no parent_event_id, fail input validation first (1605/406); an anchored query from a caller who is neither admin+ on that profile nor — on an org_id anchor — an entitled compliance auditor is 1700/403 compliance_access_required.


Event Details

GET /current/event/{event_id}/details/

Get full details for a single event.

Auth: Required (JWT). Default rate limiting. No credit consumption.

Path Parameters

ParameterTypeRequiredDescription
{event_id} string Yes Alphanumeric OpaqueId of the event

Example Request

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

Response (200 OK)

{
  "result": true,
  "event": {
    "event_id": "ancou-ywgcx-iff7k-pbijp-l4ysg-n43j",
    "category": "workspace",
    "sub_category": "transfer",
    "visibility": "external",
    "permission": "member",
    "severity": "medium",
    "notification": "affected_user",
    "description": "The file 'quarterly_report.pdf' has been added to the workspace 'Engineering' by 'Jane Smith'.",
    "event": "workspace_storage_file_added",
    "required_params": ["file_added", "workspace"],
    "node_type": "file",
    "operation_type": "add",
    "requires_parent_node": false,
    "event_profile": "1234567890123456789",
    "calling_user": "9876543210987654321",
    "object_id": "2lavr2y6uogubeyvfmqv2ur6k6ezt",
    "template": {
      "description": "The file '{{#name file_added}}' has been added to the workspace '{{#name workspace}}'{{#if calling_user}} by '{{#fullname calling_user}}'{{/if}}.",
      "params": ["2lavr2y6uogubeyvfmqv2ur6k6ezt", "1234567890123456789", "9876543210987654321", "9876543210987654321", null]
    },
    "created": "2025-01-20 10:30:45 UTC",
    "org_id": "1111111111111111111",
    "workspace_id": "1234567890123456789",
    "user_id": "9876543210987654321",
    "acknowledged": false
  }
}

Access Rules

ConditionAccess
Event is internal visibilityAlways denied
Event has targeted permissionGranted only to the event’s user_id (the target); denied to everyone else, including the calling_user
User is the calling_userGranted
User is the event’s user_idGranted
User has appropriate profile-level permissionsGranted based on permission level

Error Responses

Error CodeHTTP StatusDescription
1605 (Invalid Input)406Event ID missing, empty, or not a valid OpaqueId
1609 (Not Found)404No event exists with the provided ID
1605 (Invalid Input)406Event has internal visibility
1605 (Invalid Input)406User lacks permission to view the event
1700 (Access Forbidden)403The event’s organization blocks your location or network — params.reason:"geo_restricted"
1650 (Authentication Invalid)401Missing or invalid JWT token
1651 (Invalid Request Type)405Wrong HTTP method (only GET accepted)

Acknowledge Event

POST /current/event/{event_id}/ack/

Acknowledge (mark as read) an event for the current user. Idempotent.

Auth: Required (JWT). Default rate limiting.

Path Parameters

ParameterTypeRequiredDescription
{event_id} string Yes Alphanumeric OpaqueId of the event to acknowledge

Example Request

curl -X POST "https://api.fast.io/current/event/ancouywgcxiff7kpbijpl4ysgn43j/ack/" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true
}

Error Responses

Error CodeHTTP StatusDescription
1605 (Invalid Input)406Event ID missing or invalid
1609 (Not Found)404Event not found
1605 (Invalid Input)406Event has internal visibility
1605 (Invalid Input)406User lacks permission to view the event
1700 (Access Forbidden)403The event’s organization blocks your location or network — params.reason:"geo_restricted"
1664 (Datastore Error)500Failed to persist the acknowledgment
1650 (Authentication Invalid)401Missing or invalid JWT token

Note: Acknowledgment is per-user. Acknowledging for one user does not affect others. Same access rules as event details apply. An entitled compliance auditor who is not an org admin may also acknowledge an org_security_alert row (see Security Alerts in the Organizations reference).

Compact Responses (output=)

Every endpoint that returns event objects (search, details) 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 event (cumulative)
terseevent_id, event, category, object_id, created
standardterse + sub_category, calling_user, actor, org_id, workspace_id, user_id, share_id, acknowledged, template (containing description + params), severity, visibility, notification (plus ip / country on audit-log searches)
fullEvery field the event carries: standard + permission, description, required_params, event_profile, node_type / operation_type / requires_parent_node (file and folder events), agent_delegation, and any event-specific promoted fields

Use terse for activity feed tickers and unread-count polling — it carries the event identity (event_id), name (event), category, target object, and a timestamp (created), which is the minimum a feed row needs to render without a follow-up fetch. Use standard for the most common event list/detail views — it adds subcategory, every profile-link id (calling user, owning org/workspace/share), the acknowledgment flag, the template object (which carries the human-readable description and any template params), and the render-hint enums: severity (drives feed-row color/icon), visibility (distinguishes the Activity, Audit-Log, and internal tabs), and notification (drives bell/notification rendering). Use full (or omit the parameter) for audit-log exports and event schema introspection. 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.

Event Categories

CategoryAPI ValueDescription
UploaduploadFile upload operations
UseruserUser account events
OrganizationorgOrganization events
WorkspaceworkspaceWorkspace operations — and workspace-scoped file/folder activity. See below
ShareshareShare operations — and share-scoped file/folder activity. See below
AIaiAI/ML operations
InvitationinvitationInvitation events
EmailemailEmail-related events
BillingbillingBilling events (billing_free_trial_ended). The subscription_* events are category org, sub-category billing
MetadatametadataMetadata operations
AppsappsApplication/integration events
NodenodeAI indexing pipeline only — not file operations. See below
ServerserverServer/system-level events
ImportimportImport operations

Where File and Folder Activity Lives

File and folder activity is NOT category node. Despite the name, node covers only the AI indexing pipeline (for example node_ai_summary_created), and most of its events are internal and never returned by the API.

It lives in two categories, and filtering only one of them silently omits the other:

category=workspace — workspace-scoped storage:

workspace_storage_file_added       workspace_storage_folder_created
workspace_storage_file_updated     workspace_storage_folder_updated
workspace_storage_file_deleted     workspace_storage_folder_deleted
workspace_storage_file_purged      workspace_storage_folder_purged
workspace_storage_file_moved       workspace_storage_folder_moved
workspace_storage_file_restored    workspace_storage_folder_restored
workspace_storage_file_copied      workspace_storage_folder_copied
workspace_storage_lock_overridden

category=share — the share-scoped counterpart, a full parallel set:

share_storage_file_added           share_storage_folder_created
share_storage_file_updated         share_storage_folder_updated
share_storage_file_deleted         share_storage_folder_deleted
share_storage_file_purged          share_storage_folder_purged
share_storage_file_moved           share_storage_folder_moved
share_storage_file_restored        share_storage_folder_restored
share_storage_file_copied          share_storage_folder_copied
share_storage_lock_overridden

A consumer that filters only workspace sees no activity in any share. Query both categories for a complete picture of what happened to files.

calling_user_id vs user_id — Actor vs Subject

These two filters answer different questions and are not interchangeable:

FilterMatches OnAnswers
calling_user_idthe user who performed the action“what did this person do”
user_idthe event’s subject user — the user the event names where it names one, otherwise the owner of the org, workspace, or share the event belongs to“what happened to / about this person, or in what they own”

calling_user_id matches the attributed actor. Usually that is the person who performed the action. Where a background workflow has a responsible party, it can be that party instead — a file that a scheduled sync pulls in is attributed to the user who connected the synced folder, even though nobody clicked anything at the time, so it appears in the activity feed under their name.

⚠ But an absent calling_user_id does NOT mean nobody was responsible. Attribution is recorded per event type, and several types that a user plainly caused still carry no actor:

So calling_user_id answers “what is attributed to this person” — right for an actor-scoped audit view, and wrong for “everything that happened in this scope”, where you should omit it. Do not assume it excludes all background activity, and do not read its absence as “the system did this on its own”. Filter on user_id when you want what happened to a person.

Event Subcategories

SubcategoryAPI ValueDescription
StoragestorageFile and folder operations: move, copy, delete, restore, version restore, folder create/update, trash emptied, lock override. Uploads and file updates are transfer
CommentscommentsComment activity
MembersmembersMembership changes
LifecyclelifecycleCreate and delete events (plus File Share and signing-envelope lifecycle). Profile updates are settings; archive/unarchive is archive
SettingssettingsConfiguration changes (user_updated, org_updated, workspace_updated, share_updated)
SecuritysecuritySecurity-related events, including sign-ins (user_login, org_sso_login) and two-factor changes
AuthenticationauthenticationSSO sign-up (user_sso_signup). Sign-ins are security
AIaiAI processing events
InvitationsinvitationsInvitation management
BillingbillingSubscription and payment
AssetsassetsAsset (avatar, branding) updates
UploaduploadUpload events
TransfertransferFiles added, updated or transferred into storage (uploads, sync, cross-profile), plus download- and preview-token issuance and ZIP downloads. Ownership changes are ownership_transferred under members.
Import/Exportimport_exportImport/export operations
Quick SharequickshareQuick share events
MetadatametadataMetadata operations
APIapiAPI-related events
ArchivearchiveArchive operations
EmailemailEmail events
RenderrenderPreview/thumbnail rendering events (internal; never returned by the API)
Cloud Importcloud_importCloud import operations

Event Visibility Levels

VisibilityAPI ValueDescription
InternalinternalSystem events. Never accessible via API.
Audit Logexternal_audit_logAudit/compliance events. Targeted permission checks bypassed for admins.
ExternalexternalStandard user-facing events.

Default (no visibility parameter): returns both external_audit_log and external events, excludes internal.

Event Permission Levels

PermissionDescription
memberAny member of the profile can view
adminOnly admins of the profile can view (plus the event’s own calling_user and user_id)
targetedOnly the target user (the event’s user_id) can view — not the calling user

When querying with visibility=external_audit_log, targeted permission checks are bypassed — but the query itself is gated: it requires an org_id/workspace_id/share_id context filter AND (admin permission on that profile, or — only when the filter is org_id — the org’s compliance_auditor flag) (see the Audit-log note under Search Events).

Event History Retention

Event history is kept for a bounded window that depends on the organization’s plan — higher plans retain history for longer. Events older than the organization’s window are removed automatically, so a search over an older period returns fewer results rather than an error.

Two things follow for integrations:

An organization that has held a paid subscription keeps a longer minimum window than its current plan alone would give, so billing and dispute history stays available after a cancellation.

Event Names Reference

Workspace Storage

Share Storage

Comments

Membership

Workspace Lifecycle

Share Lifecycle

File Share Lifecycle

Durable single-file File Share events. Category share, external visibility, member permission. Anchored to the owning workspace; the affected File Share id travels in the event data as file_share_id.

Cloud Sync Import Events

Category import, sub-category cloud_import, external visibility. Permission is SPLIT, and the split is the useful part: the three import_source_sync_* events are member, because whether a graft synced is operational status a workspace member can already infer from files appearing. Everything else in this family — including both provider_identity_* events, which carry the connected account’s email address — is admin, because whose cloud account is attached is a disclosure question rather than a status one. These are the connect / configure / sync half of cloud sync: linking a provider account, creating a source against it, and running the jobs that pull provider content in. Anchored to the owning workspace via profile_id.

Provider identity (a connected cloud account):

Import sources (a configured sync connection — one remote folder synced into a workspace):

Files arriving from a sync (these are workspace events, not import ones — listed here because cloud sync is what produces them):

Category workspace, sub-category transfer, external visibility, member permission. A large first sync produces many — roughly one per file. They are attributed to the owner of the connection that grafted the folder, so a synced file reads like an upload by that user rather than appearing authorless.

Treat them as AT MOST ONE BEST-EFFORT emission per detected add or content change — not as a guarantee of one-per-change. Two edges make the stronger reading wrong in opposite directions. Emission is best-effort: if the workspace cannot be resolved at emit time nothing is sent and nothing retries, so a real change can produce ZERO events. And change detection is deliberately conservative: when the stored content fingerprint is missing or unreadable the file is assumed changed, so an event can arrive for a file whose content did not actually change. Do not use these as a ledger of what changed — use them as a prompt to re-read, and let the folder listing be the truth.

There is NO delete counterpart. Nothing is emitted when a sync removes a file that disappeared at the provider. Do not infer from silence that a file still exists — re-read the folder to establish that. This is the single most important line in this section: an absent event here means “no signal”, never “no change”.

Import & discovery jobs (the job record behind a sync run, or behind a pre-source folder listing):

source_name is MUTABLE, CLIENT-SETTABLE display text, and it is never scrubbed. It starts as the remote folder or library path inside the user’s connected cloud account (e.g. Imported to Fastio/Images), but the update endpoint lets a caller change it afterwards, so it is untrusted input rather than a reliable provider identifier — do not key anything on it. On discovery events, before a source exists, it carries the provider name instead of a folder name. It is interpolated directly into the description of every event above except the two provider_identity_* events. identity_email on those two is the connected account’s real address. Both can name something outside the workspace’s own content, and neither is redacted before the event is persisted or read. Summarise, don’t relay verbatim — same guidance as the diagnostic-field note under Cloud Sync Write-Back below.

Cloud Sync Write-Back

Category import, sub-category cloud_import, external visibility, admin permission. Emitted when a local change is pushed back to the connected cloud provider. Anchored to the owning workspace.

Event data carries profile_id, source_id, node_id and wb_id (plus error_message on _failed). wb_id identifies the write-back itself and is the same across all four events for one push, so it is what correlates a _started with its eventual _completed, _failed or _conflict. node_id cannot do that job: a node edited twice produces two write-backs sharing one node id.

These events do NOT wake a client. They raise no realtime signal — a client must READ the events feed to see them and will not be nudged. This is deliberate: they fire per FILE, so a large sync would otherwise notify once per file.

Treat diagnostic fields as sensitive and untrusted. error_message is a coarse failure class, but diagnostic text in this family can carry provider-supplied content, including the names of files inside a user’s connected cloud account. Summarise it; never relay it verbatim into a chat, ticket, or commit message, and never parse it for control flow.

AI

Metadata

Field vocabulary merges

Category metadata, sub-category metadata, external visibility, member permission. Emitted when a workspace’s field vocabulary changes shape: one field is folded into another, so the folded name stops being its own entry and resolves to the surviving field from then on.

The event data carries workspace plus both NAMES — alias_field_name, the field that was retired, and canonical_field_name, the field it now resolves to. That pair is the point of the event: a consumer holding persisted selections that named the retired field can REWRITE them to the surviving name instead of dropping them, and a cached copy of the vocabulary can be corrected without re-reading the whole listing.

Nothing reaches anyone on its own when this fires — no email, no notification, no webhook, no realtime nudge. The fold is recorded and nothing more, so a consumer that wants to react to one must POLL GET /current/events/search/ for it. Do not wait to be woken; you will not be.

It fires only when a fold actually WRITES. A pre-flight of a merge emits nothing, and neither does a merge that finds the two fields already folded together — so receiving this event always means the vocabulary really changed. The converse does NOT hold. Emission is best-effort and nothing retries, so a real fold can produce no event at all. Do not treat the stream as a ledger of vocabulary changes: treat an event as a prompt to re-read the field vocabulary, and let that listing be the truth.

Templates and saved views (retired — no longer emitted)

Metadata templates have been replaced by the workspace field vocabulary, and per-user saved views have been folded into metadata filters. Both sets of endpoints are gone. These event types are retained only so historical activity stays readable; none of them is ever emitted now, so do not build a subscription or a workflow that waits on one.

Quick Shares (deprecated — see File Share Lifecycle)

QuickShare creation is deprecated in favor of the durable File Share; these events fire only for the draining QuickShare population. New single-file sharing emits the file_share_* events above.

Invitations

User

Organization

Compliance & Audit

Mostly category org, sub-category security. Exceptions: org_compliance_auditor_changed and org_member_transfer_started / org_member_transfer_completed are sub-category members; user_login and oauth_session_created are category user (sub-category security); storage_direct_read_summary is category workspace, sub-category storage; ownership_transferred is noted on its own entry. All use external_audit_log visibility, visible to org admins and to a member holding the compliance_auditor flag — see Compliance & Audit in the Organizations reference for the endpoints that read and write these.

Enterprise SSO

Category org, sub-category security, external_audit_log visibility, admin permission — visible only to org admins, in the audit log. Covers an org’s single sign-on configuration and domain claims (GET/POST /current/org/{org_id}/sso/...).

SCIM Provisioning

Category org, sub-category members, external_audit_log visibility, admin permission — visible only to org admins, in the audit log. Raised by an identity provider’s SCIM client (/current/scim/v2/...), never by a person, so none of these three carry a calling_user — render the actor as “Identity provider”.

Billing

Event Search Examples

Recent comments in a workspace

GET /current/events/search/?workspace_id={id}&subcategory=comments

File uploads to a share in a date range

GET /current/events/search/?share_id={id}&event=share_storage_file_added&created-min=2025-12-01T06:00:00Z

Membership changes in an org

GET /current/events/search/?org_id={id}&subcategory=members

AI activity in a workspace

GET /current/events/search/?workspace_id={id}&category=ai

Unacknowledged events for a user

GET /current/events/search/?user_id={id}&acknowledged=false

Audit log events only

GET /current/events/search/?workspace_id={id}&visibility=external_audit_log&limit=100

Child events of a batch operation

GET /current/events/search/?parent_event_id=ancouywgcxiff7kpbijpl4ysgn43j&limit=100

Org Storage Change Feed

A single endpoint that returns every storage change across every workspace and share of an org the caller can read, since a cursor — the org-scale alternative to calling /events/search/ or /activity/poll/ once per workspace and once per share.

Org Storage Changes

GET /current/org/{org_id}/events/changes/

Return the org's storage changes (file/folder add, update, delete, move, copy, restore, rename, transfer, version restore, trash emptied, links, and cloud-sync file adds/updates) across every workspace and org share the caller can read, in order, after a cursor.

Auth: Required (JWT). Default rate limiting.

Path Parameters

ParameterTypeRequiredDescription
{org_id} string Yes 19-digit numeric ID of the organization

Query Parameters

ParameterTypeRequiredDefaultDescription
cursor string No — Opaque string from a previous response. Position to resume from — omit to bootstrap.
limit integer No 250 Maximum number of changes to return (1–1000)

Example Request

curl -X GET "https://api.fast.io/current/org/1111111111111111111/events/changes/?limit=500" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK)

{
  "result": true,
  "changes": [
    {
      "event_id": "ancou-ywgcx-iff7k-pbijp-l4ysg-n43j",
      "event": "workspace_storage_file_added",
      "profile_id": "4829105738291047362",
      "profile_type": "workspace",
      "object_id": "2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4",
      "created": "2026-09-25 21:40:11 UTC",
      "calling_user_id": "1382049571038475629"
    }
  ],
  "cursor": "{opaque_cursor}",
  "has_more": false,
  "profiles": {
    "version": "9b1f0c...",
    "items": [
      {"id": "4829105738291047362", "type": "workspace"},
      {"id": "5510392857104938271", "type": "share"}
    ]
  }
}

Response Fields

FieldTypeDescription
resultbooleantrue on success
changesarrayStorage changes since cursor, oldest first
changes[].event_idstringUnique event identifier. Dedupe by this — an updated row can reappear with the same event_id; the latest delivery wins
changes[].eventstringEvent name (e.g. workspace_storage_file_added) — see the Event Names Reference for the full storage set
changes[].profile_idstring19-digit ID of the workspace or share the change belongs to
changes[].profile_typestringworkspace or share
changes[].object_idstring | nullAffected object OpaqueId (file, folder, or link). null for a change that is not about a single item (e.g. trash emptied)
changes[].createdstringEvent timestamp (Y-m-d H:i:s UTC)
changes[].calling_user_idstring19-digit ID of the attributed actor. Omitted where the caller may not see who acted
cursorstringOpaque, signed, forward-only. Pass back unchanged as cursor on the next call
has_morebooleantrue means call again. Can be true even when changes is empty. If has_more is true and the returned cursor is identical to the one you sent, the newest changes are still settling — call again after about 10 seconds, not immediately
profilesobjectThe caller's current readable set
profiles.versionstringChanges when the caller joins/leaves a workspace or share, or their file visibility on a share changes. Re-list profiles.items when it does
profiles.itemsarray{id, type} for every workspace and share the caller can currently read (a workspace-folder share shadowed by its readable workspace is omitted)

Bootstrap (no cursor): returns changes: [], has_more: false, and a head cursor. Only changes recorded after this call are reported, so list profiles.items in full immediately after taking the head cursor — that listing plus the feed from here forward is the complete picture. The head cursor sits slightly behind the very newest changes (about 10 seconds), so the first follow-up call may return changes your listing already reflects; dedupe by event_id.

Error Responses

Error CodeHTTP StatusDescription
1700 (Access Forbidden)403Caller has no readable workspace or share in this org
1605 (Invalid Input)406Cursor malformed, tampered, or minted for a different caller / org / token scope (for example after the token's scopes changed). Re-bootstrap: take a fresh head cursor, then re-list profiles.items
1605 (Invalid Input)406Cursor expired — params.reason: "cursor_expired". Do a full resync: take a fresh head cursor, then re-list profiles.items
1693 (Temporarily Unavailable)503Transient read failure. Retry with the same cursor

Note: Replaces one /events/search/ or /activity/poll/ call per workspace and per share for a client that wants "every storage change in this org" — one call and one cursor cover the whole org. A very recent change may be redelivered (the feed briefly holds back the newest rows so a concurrently-committing write is not skipped) — always dedupe by event_id, keeping the latest delivery. Access is per-caller, not per-org-membership: any user with at least one readable workspace or share in the org is served, whether or not they are an org member; unscoped tokens and workspace-scoped tokens are supported, and a workspace-scoped token sees only its workspaces; share-scoped tokens are supported too — a token scoped to shares is served when at least one of its shares belongs to this org, and sees only its scoped shares' changes (reported as share changes); a token whose shares are all in other orgs gets 403. Coverage gaps to reconcile with a periodic full listing: permanent purges are not reported, a rename arrives as the _updated event, some share-only operations have no share-side event, and a very late-committing write can be missed. A change inside a workspace folder share is reported once — as the workspace's change to a caller who can read the workspace, and as the share's change to a caller who can only read the share. See the org channel storage field under WebSocket (Real-Time) for the realtime nudge that tells you when to call this endpoint.

Activity Polling

Long-poll endpoints for efficient change detection. The server holds the connection open and returns immediately when something changes, avoiding expensive resource polling.

Poll User Activity

GET /current/activity/poll/

Poll for activity updates on the current user's profile.

Auth: Required (JWT). Subject to global rate limiting; see the rate-limit section in the main reference. No credit consumption.

Query Parameters

ParameterTypeRequiredDefaultDescription
wait integer No 0 Long-poll timeout in seconds (0–95). Server holds connection open until update or timeout.
lastactivity string No Current time Only return activity newer than this timestamp. Micro-precision datetime (e.g., 2025-01-20 10:30:45.123456 UTC).
updated any No — Any non-empty value other than 0 enables it: only return activity fields updated since lastactivity
fields string No All fields Comma-delimited activity field names to check. Max 30 fields. Supports ID qualifier via colon (e.g., storage:12345).

Example Request

curl -X GET "https://api.fast.io/current/activity/poll/?wait=30&lastactivity=2025-01-20%2010:30:45.123456" \
  -H "Authorization: Bearer {jwt_token}"

Response (200 OK — activity found)

{
  "result": true,
  "results": 3,
  "activity": {
    "storage": "2025-01-20 10:30:45.123456 UTC",
    "members": "2025-01-20 09:15:22.654321 UTC",
    "settings": "2025-01-19 14:00:00.000000 UTC"
  },
  "lastactivity": "2025-01-20 10:30:45.123456 UTC"
}

Response (200 OK — no activity)

{
  "result": true,
  "results": 0,
  "activity": []
}

Response Fields

FieldTypeDescription
resultbooleantrue on success
resultsintegerNumber of activity fields returned
activityobject or arrayMap of activity field names to micro-precision UTC timestamps. When results is 0 this is an empty array [], not {}.
lastactivitystringMost recent timestamp; pass as lastactivity in next poll. Omitted when results is 0.

Error Responses

Error CodeHTTP StatusDescription
1605 (Invalid Input)406Invalid profile ID format
1605 (Invalid Input)406User lacks permissions for the specified profile
1605 (Invalid Input)406Invalid field names or more than 30 fields
1665 (Object Init Failed)500Internal error
1650 (Authentication Invalid)401Missing or invalid JWT token

Poll Profile Activity

GET /current/activity/poll/{profile_id}/

Poll for activity updates on a specific workspace, share, org, File Share, or Sign Envelope.

Auth: Required (JWT). Same rate limits and parameters as Poll User Activity.

Path Parameters

ParameterTypeRequiredDescription
{profile_id} string Yes Profile ID to subscribe to (org, workspace, share, File Share, or Sign Envelope) — a 19-digit numeric profile ID; a File Share may also be addressed by its opaque id_alt. For upload progress, use your user ID.

Access Requirements

Profile TypePermission Required
User (self)Authenticated
OrganizationView permission on the org
WorkspaceView permission on the workspace
SharePermission to view the share's details
File ShareThe same access decision as the File Share's public read surface (access tier + password + named grant). A signed-in recipient granted view/download/edit receives a recipient-scoped feed (comment activity plus a bare content/lifecycle nudge — never the owner's internal activity); an anonymous anyone-with-link visitor cannot poll.
Sign EnvelopeThe e-sign creator-side realtime channel. Requires view access to the sign envelope.

Example Request

curl -X GET "https://api.fast.io/current/activity/poll/1234567890123456789/?wait=30&fields=storage,members&updated=1&lastactivity=2025-01-20%2010:30:45.123456" \
  -H "Authorization: Bearer {jwt_token}"

Response format is identical to Poll User Activity.

Org access/AI policy. For an Org, Workspace, or Share profile owned by an Enterprise org with a restriction configured, a blocked caller gets 403 geo_restricted and an MCP-classified caller blocked by mcp_access gets 403 mcp_access_denied, ahead of the usual profile-lookup error; a policy verdict that cannot be read is 503 access_policy_unavailable. See Access Policy (Geo / IP Restrictions) and AI, Intelligence & MCP Access Policy in the Organizations reference.


Polling Workflow

  1. Make initial poll request (no lastactivity parameter)
  2. Receive response with activity fields and lastactivity timestamp
  3. Process changes by fetching updated resources based on activity field names
  4. Make next poll with lastactivity from previous response
  5. Repeat — server returns immediately on change, or after wait seconds timeout

Activity Key Patterns

Key PatternWhat Changed
storage:{fileId}File added, updated, or removed
preview:{fileId}File preview/thumbnail is ready
metadata:{fileId}File's extracted metadata fields were written (use to refresh a single row during a template extraction)
ai_chat:{chatId}AI chat message updated
comments:{nodeId}Comment added or updated
membershipA member was added, removed, or had their role changed

Anti-Patterns

WebSocket (Real-Time)

Optional real-time delivery (~300ms latency vs ~1s for polling). Sends both activity messages (field names for change detection) and enriched event messages (full event details) via WebSocket. Backwards compatible — existing clients continue to work without changes.

WebSocket Auth

GET /current/websocket/auth/{profile_id}

Generate a WebSocket authentication JWT for a specific profile. User, organization, and workspace tokens are valid for 1 hour, Sign Envelope tokens for 24 hours. Share and File Share tokens use a shorter TTL (30 minutes) — always check the expires_in field on every response and refresh before it expires. A File Share realtime channel is gated by the same access decision as its public read surface (access tier + password + named grant), the same requirement as its activity poll. A signed-in recipient granted view/download/edit can mint a token and receives a recipient-scoped feed — comment activity plus a bare content/lifecycle nudge, never the owner's internal activity; an anonymous anyone-with-link visitor cannot mint a realtime token.

Auth: Required (JWT). No credit consumption.

Path Parameters

ParameterTypeRequiredDescription
{profile_id} string Yes ID of the user, org, workspace, share, file share, or sign envelope to subscribe to — a 19-digit numeric ID; a File Share may also be addressed by its opaque id_alt

Example Request

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

Response (200 OK)

{
  "result": true,
  "expires_in": 3600,
  "auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Response Fields

FieldTypeDescription
resultbooleantrue on success
expires_inintegerEffective token lifetime in seconds (3600 = 1 hour for user/org/workspace; 1800 = 30 minutes for share and file-share; 86400 = 24 hours for sign envelope)
auth_tokenstringSigned JWT with websocket scope, bound to the requested profile

Access Requirements

Profile TypePermission Required
User (self)None beyond authentication
OrganizationView permission on the org
WorkspaceView permission on the workspace
SharePermission to view the share's details
File ShareThe same access decision as the File Share's public read surface (access tier + password + named grant), the same gate as its activity poll. A signed-in view/download/edit recipient mints a recipient-scoped token (comment activity plus a bare content/lifecycle nudge only); an anonymous anyone-with-link visitor cannot mint one.
Sign EnvelopeView access to the sign envelope (the e-sign creator-side realtime channel)

Token Lifetime

The default expires_in for User, Organization, and Workspace tokens is 3600 (1 hour). Share and File Share tokens are shorter-lived (1800 seconds / 30 minutes) — always inspect the expires_in field on the response and refresh the token before it expires. These channels use a short TTL because the recipient's standing can change during a session, and the token freezes that standing at mint time: a share guest can be removed or have their access level downgraded, and a File Share recipient's grant or the file's access tier/password can change. The short TTL bounds how long an open connection can keep delivering events under a now-stale authorization; on reconnect the freshly minted token reflects the current standing. User, Organization, and Workspace tokens are bounded to an hour for the same reason: once a sign-in session ends (sign out, sign out everywhere, or a password change), requests made with it can no longer mint tokens, so a connection opened through it ends shortly after its token expires. When a user, organization, workspace, share or file-share token expires the server closes the open connection with a denied frame; mint a fresh token and reconnect.

Error Responses

Error CodeHTTP StatusDescription
1605 (Invalid Input)406No profile ID provided or invalid format
1605 (Invalid Input)406Profile type unsupported, not found, or user lacks permissions
1650 (Authentication Invalid)401Missing or invalid JWT, or internal JWT generation failure
(generated per call site)403geo_restricted — the Org/Workspace/Share profile is owned by an Enterprise org whose access policy blocks the caller’s location or network. See Access Policy (Geo / IP Restrictions) in the Organizations reference.
(generated per call site)403mcp_access_denied — an MCP-classified caller is blocked by the org’s mcp_access policy. See AI, Intelligence & MCP Access Policy in the Organizations reference.
(generated per call site)503access_policy_unavailable — the access-policy verdict could not be read; retry.

WebSocket Connection

Connect to: wss://{host}/api/websocket/?{auth_token}

Where {auth_token} is the JWT returned from the auth endpoint above — it is the entire query string (no token= key, no other query parameters).

Message Types

The server pushes two types of JSON messages:

activity Messages

Indicate which resource categories changed. Use these to know what to re-fetch:

{
  "response": "activity",
  "activity": ["storage:2abc...", "preview:2abc..."]
}

The activity array contains the same activity key patterns as the polling endpoint.

Org Channel storage Field

On an organization channel only, a bare storage key (no :{id} suffix) is pushed at most about once a second per org whenever a storage change occurs in any workspace or share of the org:

{
  "response": "activity",
  "activity": ["storage"]
}

It is deliberately content-free — the org channel reaches every org member, including members who cannot read the workspace or share that actually changed, so it never names the change. On receipt, call GET /current/org/{org_id}/events/changes/ to learn what happened, and call it again roughly a second later to catch anything that committed just after the ping (there is no trailing signal). Keep a slow periodic call to the same endpoint as a safety net alongside the org channel.

event Messages

Sent alongside activity messages when structured event data is available. Provide full event details for immediate UI updates without a follow-up API call:

{
  "result": true,
  "response": "event",
  "time": "2026-03-22 14:30:45.1234",
  "timestamp": "1711123456.1234",
  "event": "workspace_storage_file_added",
  "category": "workspace",
  "subcategory": "storage",
  "object_id": "23s5ktto3hoomtz3fbgrmhurl2mi6",
  "calling_user_id": "1234567890123456789",
  "activity_field": "storage",
  "data": {
    "name": "report.pdf",
    "parent_node_id": "xyz789...",
    "size": 1048576
  }
}
event Message Fields
FieldTypeDescription
resultbooleanAlways true for event messages
responsestringAlways "event" for this message type
timestringServer send time (YYYY-MM-DD HH:MM:SS.<fraction> with no UTC suffix and a variable number of fraction digits, e.g. "2026-03-22 14:30:45.1234"). Added when the message is dispatched.
timestampstringEvent trigger time as a microtime float string (e.g., "1711123456.1234").
eventstringEvent name identifier (e.g., workspace_storage_file_added)
categorystringEvent category (e.g., workspace, share)
subcategorystringEvent subcategory (e.g., storage, members)
object_idstringAffected object OpaqueId (file, folder, etc.), in its unhyphenated form; empty string when the event has none
calling_user_idstring19-digit numeric ID of the user who triggered the event; empty string when none was recorded
activity_fieldstringCorresponding activity field name (e.g., storage)
dataobjectEvent-specific details; shape varies by event type
Backwards Compatibility

activity messages are sent for every change a recipient is permitted to observe. event messages are supplementary — sent alongside activity messages when structured event data is available. Existing clients need no changes. Event payloads are kept under ~4KB.

Permission gating: Enriched event messages are only sent for member-level events. Admin and targeted events receive only the activity message.

Per-recipient scoping (share channels): On a share channel, each outgoing activity/event frame is scoped to the receiving guest's share access before it is delivered. A guest receives only the changes their access level permits them to observe — frames they may not see are suppressed, and content detail (file names, node identifiers, enriched data) is collapsed to a bare category or dropped for guests with restricted file visibility. Members and all non-share channels (user / organization / workspace) are unaffected and receive the full frame. Do not assume a share guest will observe every change on the channel, or that a delivered activity frame will always carry an enriched event companion.

Fallback

If the WebSocket connection drops, fall back to long-polling (GET /current/activity/poll/{profile_id}/). Activity polling returns field names and timestamps. To retrieve full event details after reconnection, use GET /current/events/search/ with a created-min filter.

Realtime Auth (Collaborative Rooms)

Separate from WebSocket activity channels, these endpoints provide authentication for collaborative editing rooms.

Generate Realtime Token

GET /current/realtime/auth/{room_id}

Generate a realtime JWT for a workspace or share collaborative room. Tokens are valid for 24 hours.

Auth: Required (JWT). Subject to global rate limiting; see the rate-limit section in the main reference. No credit consumption.

Path Parameters

ParameterTypeRequiredDescription
{room_id} string Yes 19-digit numeric ID of the workspace or share to join

Example Request

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

Response (200 OK)

{
  "result": true,
  "expires_in": 86400,
  "auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Access Requirements

Profile TypePermission Required
WorkspaceAt least View permission
SharePermission to view the share's details + multiplayer enabled

Error Responses

Error CodeHTTP StatusDescription
1605 (Invalid Input)406Missing room ID
1605 (Invalid Input)406Room ID is not numeric
1605 (Invalid Input)406Room ID does not correspond to a workspace or share
1680 (Access Denied)401User lacks permissions on the room
1650 (Authentication Invalid)401Missing or invalid JWT, or internal JWT generation failure

Note: Only workspace and share profile types are accepted as room IDs.


Generate Realtime Note Token

GET /current/realtime/note-auth/{profile_id}/{note_id}

Generate a realtime token for collaborative editing of a single Note. The token is bound to the calling user, the note's workspace, and the note itself, and carries the caller's edit/view standing for the note. Tokens are valid for 900 seconds (15 minutes); refresh before expiry by calling the endpoint again, which re-resolves your current standing on the note.

Auth: Required (JWT). Subject to global rate limiting; see the rate-limit section in the main reference. No credit consumption.

Path Parameters

ParameterTypeRequiredDescription
{profile_id} string Yes 19-digit numeric ID of the workspace the note lives in
{note_id} string Yes ID of the Note to edit

Example Request

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

Response (200 OK)

{
  "result": true,
  "expires_in": 900,
  "auth_token": "{realtime_note_token}"
}

Response Fields

FieldTypeDescription
resultbooleantrue on success
expires_inintegerToken lifetime in seconds (900 = 15 minutes)
auth_tokenstringSigned realtime-note token, bound to the user, workspace, and note

Access Requirements

The token's granted capabilities are frozen at mint time from your current permission on the workspace:

Workspace PermissionGranted Capability
Edit (or higher)Read and edit the note
ViewRead the note only
Below ViewDenied

Because the capabilities are frozen for the token's lifetime, a permission change made after a token is issued takes effect on the next token refresh (at most ~15 minutes later).

Error Responses

Error CodeHTTP StatusDescription
1605 (Invalid Input)406Missing or invalid note ID, or the node is not a note
1609 (Not Found)404No such note exists, or the note is in the trash
1680 (Access Denied)401You lack permission on the workspace
1650 (Authentication Invalid)401Missing or invalid JWT, or internal token generation failure

Validate Realtime Token

GET /current/realtime/auth/validate/

Validate a realtime JWT and extract the room ID. Intended for backend services to verify tokens.

Auth: Bearer token in Authorization header (the realtime JWT to validate, not a user JWT).

Example Request

curl -X GET "https://api.fast.io/current/realtime/auth/validate/" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Response (200 OK)

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

Response Fields

FieldTypeDescription
resultbooleantrue on success
room_idstringThe workspace or share profile ID the token is bound to

Error Responses

Error CodeHTTP StatusDescription
1650 (Authentication Invalid)401Missing Authorization header
1605 (Invalid Input)406Malformed Authorization header
1605 (Invalid Input)406Authorization header does not use Bearer scheme
1605 (Invalid Input)406Bearer keyword present but no token follows
1605 (Invalid Input)406Token is not valid JWT format
1680 (Access Denied)401Token signature verification or expiration check failed
1610 (Internal Error)500Token payload is malformed or missing required fields
1605 (Invalid Input)406Token scope is not realtime

Notes: Only validates tokens with realtime scope. WebSocket-scoped tokens are rejected. Validation is performed using the JWT alone.

Validate Realtime Note Token

GET /current/realtime/note-auth/validate/

Validate a realtime-note token (minted by GET /current/realtime/note-auth/{profile_id}/{note_id}) and read back the note, workspace, and permission it is bound to. Intended for the collaborative-editing backend to confirm a token on connect.

Auth: Bearer token in Authorization header (the realtime-note token to validate, not a user JWT).

Example Request

curl -X GET "https://api.fast.io/current/realtime/note-auth/validate/" \
  -H "Authorization: Bearer {realtime_note_token}"

Response (200 OK)

{
  "result": true,
  "profile": "1234567890123456789",
  "node": "2ik5q-a43cm-uixi2-van5r-3eolo-7mue",
  "perm": "edit",
  "file_share_id": null
}

Response Fields

FieldTypeDescription
resultbooleantrue on success
profilestringThe workspace profile ID the token is bound to
nodestringThe Note node ID the token is bound to
permstringThe frozen permission: "edit" (read and edit) or "view" (read only)
file_share_idstring|nullThe File Share ID the token was minted for when it is a File Share-surface note token; null for a workspace-native token. The realtime backend uses this to route note reads/saves to the File Share-specific endpoints rather than the workspace-native ones.

Error Responses

Error CodeHTTP StatusDescription
1650 (Authentication Invalid)401Missing Authorization header, or the token is invalid, expired, wrong-scope, wrong-audience, or malformed

Notes: Only validates tokens with the realtime-note scope; any other token is rejected. Validation is performed using the token alone — signature, expiry, scope, audience, and the perm/capability binding are all checked.

↑ Back to top