Share.Ninja API (1.0.0)

Download OpenAPI specification:

Errors

Every error response is one envelope:

{ "code": "not_found", "status": "link_not_found", "error": null }

code is the category (bad_request, unauthorized, payment_required, forbidden, not_found, conflict, gone, rate_limited, payload_too_large, internal, unsupported) or a domain code with its own typed details, such as storage_quota_exceeded. status names the exact condition. error is an optional diagnostic text and details an optional typed payload. The complete list of codes, statuses and details shapes is the x-error-catalog on the ErrorResponse schema. Handle an unknown code by its HTTP status.

Rate limiting

Requests are limited per client IP before authentication and per actor after it. A rejected request answers 429 with status rate_limit_exceeded and the headers Retry-After, RateLimit-Remaining and RateLimit-Reset.

CSRF

A state-changing request (POST, PUT, PATCH, DELETE) authorized by the session cookie must carry the X-CSRF-Token header with the value of the csrf_token cookie. The server sets both cookies together, starting with POST /auth/agents. A missing or invalid token answers 403 with status csrf_required or csrf_invalid. Bearer requests need no token. Operations marked x-csrf-exempt (register, token refresh, login start, challenge verify, token revoke, the SSO exchanges) never require it, so a stale cookie cannot block a re-login.

Realtime

State changes are also pushed over one server-to-client WebSocket; REST stays the source of truth. Connect with GET /ws/connect?ticket=… (browser, ticket from POST /auth/agents) or GET /ws/action with a Bearer token (native). Subscriptions are managed over REST (POST /events/subscriptions), belong to the caller rather than the socket, and survive reconnects. Topics are user:<user_id>, workspace:<workspace_id>, link:<url_token> and link:* (every link the caller owns); the caller's own agent:<agent_id> inbox is always delivered.

Every frame is one WSMessage variant: a flat object whose dotted type selects the shape, with id, topic and timestamp beside the variant's own fields. Frames are stored server-side: reconnect with last_seen_id to receive what was missed, then the live stream continues. Past the retention window re-read the resource over REST. Each subscriber receives a frame once, even when it is published to several topics.

Idempotency

POST /links/{url_token}/acl/grant repeated with the same body answers status: unchanged; a different body for the same subject answers 409 already_granted_different. Billing mutations take an Idempotency-Key header: the same key with the same request replays the original operation, the same key with a different request answers 409. Deciding a vault join request that is already decided returns its original outcome.

Cloud transfers

Cloud access is per-file presigned URLs; there is no credential to refresh. To renew an expired URL or add files, call POST /links/{url_token}/{direction}/presign. A large file uses the multipart flow: upload/multipart/begin, upload/multipart/parts (repeatable), then upload/multipart/complete or upload/multipart/abort; list-uploads and list-parts resume an interrupted upload.

Login and step-up

POST /auth/sessions with an email always answers a challenge listing the account's sign-in methods, in the same shape for known and unknown addresses. Verify any one of them with POST /auth/challenges/{token}; with two-step verification on, a password or email-code sign-in is followed by any one other factor from the list the response carries, while a passkey completes the login on its own. A code by email or SMS is sent on request (POST /auth/challenges/{token}/code), not by the call that lists it. A sensitive operation called without a fresh factor answers 403 challenge_required with a challenge bound to that call; verify it and retry with the token in the X-Auth-Challenge header.

Mass session revoke

POST /auth/sessions/revoke-all ends the user's other sessions at once, sparing the calling one; when the calling device is in the vault's trust circle, every other device leaves the circle in the same call. Account suspension, vault recovery and vault reset end them all. The explaining frame (session.revoked_all, auth.user_suspended) is delivered before the socket closes and is replayed on reconnect. Treat an unknown reason or category as the generic case.

Auth

Device registration, login, sessions and tokens. POST /auth/agents registers the device and returns an access token, a refresh token and a WebSocket ticket. Login starts with POST /auth/sessions and completes through POST /auth/challenges/{token}, one factor per call; the resulting access token carries both the agent and the user. Third-party access uses the OAuth authorization-code flow (/auth/authorize, POST /auth/token). SSO with Google and Apple runs through /auth/sso/{provider} on the web and POST /auth/sso/{provider}/token with a native ID token. Refresh tokens rotate on every refresh. Factor types: password, email_code, webauthn, totp, sms, device_push; two-step verification and recovery codes live under /users/me/auth/.

Register a device

Registers the calling device and issues its session. device_public_key is the device identity and its trust-circle key: the same key refreshes the same agent, a different key creates a new one.

Returns agent_id, name and a one-time WebSocket ticket valid for 30 seconds — connect to GET /ws/connect?ticket=… at once. With response_mode=token the session comes back as access_token and refresh_token, otherwise as cookies. name and nearby_visibility are stored when sent and untouched when omitted; a new device starts at everyone. Sign in with POST /auth/sessions.

query Parameters
response_mode
string
Default: "cookie"
Enum: "cookie" "token"
Example: response_mode=token

Where the issued tokens go. cookie, the default, sets them as cookies for a browser; token returns them in the response body for a native or CLI client.

Request Body schema: application/json
required
name
string <= 255 characters

Device name shown in the interface. Auto-generated when empty.

client_build
string <= 64 characters

Build version of the client application

object

Device metadata. product_id, device_os and device_os_version are shown in the sessions and devices lists; the rest is diagnostic.

device_public_key
required
string <byte> <= 64 characters

X25519 public key that identifies the device and serves as its trust-circle key. Raw key bytes in base64, exactly 32 bytes after decoding; a register without it is refused.

nearby_visibility
string (NearbyVisibility)
Enum: "everyone" "nobody"

Who may discover this device in nearby lists.

  • everyone — the device appears in the nearby list of every peer sharing its bucket. Default for a newly registered device.
  • nobody — the device is absent from every peer's nearby list. It still receives its own full nearby list, still sends share offers, and still accepts offers addressed to its agent_id: the switch governs discovery, not delivery.

An agent is identified by device plus network, so a device registering from a different network starts at everyone again; a client that wants the mode to follow the device sends it on every POST /auth/agents, like the name.

instance_id
string^[-a-zA-Z0-9_]{1,64}$

Opaque id of the client runtime this register runs in; a web client makes a fresh one per tab. A link bound to it is served only while a socket of that instance is live. Native clients omit it.

Responses

Request samples

Content type
application/json
{
  • "name": "My MacBook",
  • "client_build": "1.2.3",
  • "device_info": {
    },
  • "device_public_key": "q8Gk1GMnfN3WjLKBHaOvhB0tXRuQbVJLBqFz5mFqHQA=",
  • "nearby_visibility": "everyone",
  • "instance_id": "tab-9f2c4b1a"
}

Response samples

Content type
application/json
{
  • "agent_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "access_token": "string",
  • "refresh_token": "string",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "name": "Crimson Falcon",
  • "ticket": "x7KPqRsT2uVwXyZ9aBcDeFgHiJkLmNoP1234567890AB",
  • "ticket_expires_in": 30,
  • "encryption_public_key": "string",
  • "signing_public_key": "string",
  • "encrypted_identity_key": "string",
  • "vault_status": "not_initialized"
}

OAuth 2.0 token endpoint

Exchanges a refresh token, or an authorization code from the consent flow, for a new access token. The grant type selects the body shape; see TokenRequest.

A refresh token is rotated on every use: the response carries a new one and the token sent stops working. Replaying an already-rotated token past the grace window answers 401 refresh_token_reused and revokes the session, so the client must authenticate again.

Device tokens are issued by POST /auth/agents and user tokens by POST /auth/challenges/{token}; this endpoint only renews them.

query Parameters
response_mode
string
Default: "cookie"
Enum: "cookie" "token"
Example: response_mode=token

Where the issued tokens go. cookie, the default, sets them as cookies for a browser; token returns them in the response body for a native or CLI client.

Request Body schema: application/json
required
grant_type
required
string

Must be "refresh_token"

refresh_token
required
string

The refresh token to exchange

Responses

Request samples

Content type
application/json
Example
{
  • "grant_type": "refresh_token",
  • "refresh_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6"
}

Response samples

Content type
application/json
{
  • "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidXNyXzEyMzQ1Njc4IiwibW9kZSI6InVzZXIiLCJleHAiOjE3MDI2NTYwMDB9.signature",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "refresh_token": "b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a1",
  • "scope": "user"
}

Revoke a refresh token

Revokes a refresh token per RFC 7009, ending the session it belongs to. Answers 204 whether or not the token was valid, so the response cannot be used to probe a token.

Request Body schema: application/json
required
token
required
string

The refresh token to revoke

token_type_hint
string
Value: "refresh_token"

Responses

Request samples

Content type
application/json
{
  • "token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6",
  • "token_type_hint": "refresh_token"
}

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

Log out and clear the session

Revokes the refresh token taken from the body, or from the refresh_token cookie when the body carries none, and clears the session cookies.

Request Body schema: application/json
refresh_token
string

Refresh token to revoke. The cookie is used when it is absent.

Responses

Request samples

Content type
application/json
{
  • "refresh_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6"
}

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

Read consent request details

Returns the details of an authorization request so the client can render the consent screen. There is no client registration: code_challenge is required, and the matching verifier is checked when the code is exchanged.

query Parameters
redirect_uri
required
string
Example: redirect_uri=https://app.example.com/callback

Where to redirect after consent

code_challenge
required
string [ 43 .. 44 ] characters ^[A-Za-z0-9_-]{43}=?$
Example: code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

PKCE code challenge — S256, base64url of the SHA-256 hash, 43 characters, a trailing = tolerated

code_challenge_method
string
Default: "S256"
Value: "S256"
Example: code_challenge_method=S256
scope
string
Example: scope=user

Requested scopes, space-separated

state
string
Example: state=xyz123

Opaque value returned unchanged in the redirect

agent_id
string
Example: agent_id=agt_9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f

The requesting client's own device. When set, the code and the session its exchange mints belong to this device rather than to the one that approves the consent. Echoed back so the consent page can pass it to the POST, together with the device's registered card (display_name, product_id, device) so the page can show which app is asking. Must name a registered device; 400 otherwise.

Responses

Response samples

Content type
application/json
{
  • "scope": "user",
  • "scopes": [
    ],
  • "state": "xyz123",
  • "agent_id": "agt_9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  • "display_name": "My MacBook",
  • "product_id": "com.shareninja.desktop",
  • "device": {
    }
}

Submit OAuth consent

Records the user's consent decision and returns the redirect URL carrying the authorization code, which POST /auth/token exchanges together with the PKCE verifier. Requires a signed-in user.

The code belongs to the device named by agent_id when it is given — the requesting client's own device, in the same app as the approving session — and to the approving device otherwise; the exchanged session is minted for that device.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
redirect_uri
required
string
code_challenge
required
string [ 43 .. 44 ] characters ^[A-Za-z0-9_-]{43}=?$

PKCE code challenge — S256, base64url of the SHA-256 hash, 43 characters, a trailing = tolerated

code_challenge_method
string
Default: "S256"
Value: "S256"
scope
string
state
string
approve
required
boolean

true approves the request, false denies it

agent_id
string

The requesting client's own device, echoed from the GET. When set, the code and the exchanged session belong to it rather than to the approving device.

Responses

Request samples

Content type
application/json
{
  • "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
  • "code_challenge_method": "S256",
  • "scope": "user",
  • "state": "xyz123",
  • "approve": true,
  • "agent_id": "agt_9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f"
}

Response samples

Content type
application/json

List active sessions Deprecated

Deprecated: use GET /users/me/devices, which returns the same sessions attached to their device cards.

Returns the authenticated user's sessions that are not revoked. Each carries its id for selective revocation, the address and user agent it was created with, when it was created and last used, and whether it is the calling session.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Start login or signup

Starts authentication for an email address. An address with no account is signed up by this very call; there is no separate registration step.

The response always carries a challenge listing the account's sign-in methods, in a shape identical for a known and an unknown address, so registered addresses cannot be enumerated. No session is issued here, and no email code is sent unless email_code is the only method: otherwise request it with POST /auth/challenges/{token}/code once the user picks it. An account that removed its email_code factor lists no such entry; a lost password is reset through POST /auth/recovery.

Verify one method with POST /auth/challenges/{token}; the response completes the login or lists the second step.

Request Body schema: application/json
required
email
required
string <email> <= 320 characters

Account email address

remember_me
boolean
Default: false

When true, the issued session gets the deployment's extended lifetime.

accept_terms
boolean
Deprecated

Accepted and ignored. Signup is implicit: an unknown email is signed up by this very call.

display_name
string or null <= 255 characters

Display name for a new account. Derived from the email when absent, and ignored for an account that already exists.

invite_token
string or null [ 6 .. 128 ] characters

Invite token from an /invite/{token} link. Signing up with it creates the account inside the invited workspace, under the address the invite was issued to.

time_zone
string <= 64 characters

IANA name of the time zone the client is in, such as Europe/Berlin. A new account keeps it as its own; an existing one is not changed. A name the IANA database does not hold answers 400.

Responses

Request samples

Content type
application/json
Example

Send the email address to start the flow

{
  • "email": "user@example.com"
}

Response samples

Content type
application/json
Example

The user picks the password or the email code; the code is only mailed if requested with POST /auth/challenges/{token}/code

{
  • "challenge": {
    }
}

Log out of the current session

Revokes the caller's own session together with its refresh tokens, and clears the session cookies.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Revoke a specific session

Revokes the named session and its refresh tokens, signing out the device that holds it.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
session_id
required
string
Example: 018f7b2e-3c4d-7a1b-9e6f-2a5c8d1e4b7a

Identifier of the session to revoke

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Revoke all other sessions

Revokes every other session of the current user: every other signed-in device is signed out, while the calling session stays signed in and keeps its credential.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Request a step-up challenge

Always answers 403 challenge_required: a challenge is never minted out of band.

To confirm a sensitive operation, call that operation without confirmation. Its 403 challenge_required refusal carries a challenge bound to that exact call; verify it with POST /auth/challenges/{token} and retry the operation with the token in the X-Auth-Challenge header.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "token": "ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5",
  • "expires_at": "2025-05-20T10:35:00Z",
  • "sign_in_methods": [
    ]
}

Verify an authentication challenge

Verifies one entry of the challenge named by token: a sign-in method first, then — when the response lists second_factors — one of those. The call needs a registered device: its access credential names the device the session is minted on.

For a login or signup, the step that completes it issues the session. With response_mode=token it comes back as access_token and refresh_token, otherwise it is set as cookies.

For a step-up, complete: true means the path token is now spendable: retry the operation with it in the X-Auth-Challenge header.

A wrong credential, or a factor_id the account does not have, answers 401 invalid_credentials and counts against the attempt limit.

A recovery with two-step verification on whose second step nothing is left to answer is refused with 409 no_second_factor.

path Parameters
token
required
string
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token to verify

query Parameters
response_mode
string
Default: "cookie"
Enum: "cookie" "token"
Example: response_mode=token

Where the issued tokens go. cookie, the default, sets them as cookies for a browser; token returns them in the response body for a native or CLI client.

Request Body schema: application/json
required
factor_id
required
string

The id of the challenge entry being verified. The email_code and recovery_code entries carry their type as the id.

required
PasswordCredentials (object) or CodeCredentials (object) or WebAuthnCredentials (object) or RecoveryCodeCredentials (object)

Credentials for the entry named by factor_id

Responses

Request samples

Content type
application/json
Example
{
  • "factor_id": "totp_abc123def456xyz7",
  • "credentials": {
    }
}

Response samples

Content type
application/json
Example
{
  • "complete": true,
  • "access_token": "abc123xyz789...",
  • "user": {
    }
}

Send the code for a challenge entry

Sends the code for one code-delivering entry of the challenge named by token — the email_code entry by email, an sms entry by text — a fresh one on every call, so it also resends. Each send draws on the address's code budget. An entry the challenge does not list at its current step, or one that delivers no code, answers factor_not_allowed.

path Parameters
token
required
string
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token to send the code for

Request Body schema: application/json
required
factor_id
required
string

The id of an email_code or sms entry the challenge lists at its current step.

Responses

Request samples

Content type
application/json
Example
{
  • "factor_id": "email_code"
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Start a password recovery

Starts a password recovery for an email address and mails its code at once. The response is a challenge whose only entry is email_code, in a shape identical for a known and an unknown address; an unknown one gets no mail. Verify the code with POST /auth/challenges/{token}: with two-step verification on, the second step follows as for a login, without the password: any other confirmed factor or a recovery code. With nothing left to answer it, verify refuses the recovery (no_second_factor). A completed recovery issues no session; it answers complete with the token that POST /auth/recovery/{token}/password spends.

Request Body schema: application/json
required
email
required
string <email> <= 320 characters

Account email address

Responses

Request samples

Content type
application/json
{
  • "email": "user@example.com"
}

Response samples

Content type
application/json
{
  • "token": "ch_rec123abc456def789",
  • "expires_at": "2025-05-20T10:35:00Z",
  • "sign_in_methods": [
    ]
}

Set a new password after a recovery

Sets the account's password from a completed recovery challenge, replacing the existing one, and ends every session of the account. The token is spent by this call. An unknown, unverified or already spent token answers invalid_challenge_token; one past its time, challenge_expired. A completed recovery opens no session: the client signs in with the new password.

path Parameters
token
required
string
Example: ch_rec123abc456def789

The recovery challenge token, verified

Request Body schema: application/json
required
password
required
string

The new password, in the same form AuthFactorCreateRequest takes it: 8 to 128 characters, or password_too_short / password_too_long.

Responses

Request samples

Content type
application/json
{
  • "password": "a]3N$k9Lm#pQ2wX"
}

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

List available SSO providers

Returns the SSO providers enabled for this deployment, each with the label to show on its sign-in button.

Responses

Response samples

Content type
application/json
{
  • "providers": [
    ]
}

Start SSO authentication flow

Returns the provider's authorization URL for the client to open, and sets the state cookie the callback checks. The provider then redirects to /auth/sso/{provider}/callback with an authorization code.

The call must carry the calling device's access credential — the SSO login binds to that device — and answers 401 without one. An expired access token is still accepted here, because the round trip through the provider can outlast its 15-minute lifetime.

path Parameters
provider
required
string
Enum: "google" "apple"

SSO provider name

query Parameters
redirect_uri
string

Where to return once SSO completes: a path on this origin, a native app scheme, or an allow-listed URL.

Responses

Response samples

Content type
application/json
{
  • "auth_url": "string"
}

Complete SSO redirect callback

Completes the provider's redirect: checks state against the state cookie, exchanges the code with the provider, and establishes the session on the device the flow started from. Session cookies are set either way; with response_mode=token the profile is returned as JSON instead of the redirect.

path Parameters
provider
required
string
Enum: "google" "apple"
query Parameters
code
required
string

Authorization code from the provider

state
required
string

State value, checked against the state cookie

response_mode
string
Default: "cookie"
Enum: "cookie" "token"
Example: response_mode=token

Where the issued tokens go. cookie, the default, sets them as cookies for a browser; token returns them in the response body for a native or CLI client.

Responses

Response samples

Content type
application/json
{
  • "user": {
    },
  • "is_new_user": false
}

Complete SSO callback by form post

Completes the flow for a provider that posts the callback as form data instead of redirecting with query parameters, which is how Apple Sign-In returns. Otherwise it behaves like the GET callback.

path Parameters
provider
required
string
Value: "apple"
Request Body schema: application/x-www-form-urlencoded
required
code
string

Authorization code

state
string

State value, checked against the state cookie

id_token
string

ID token, which Apple supplies directly

user
string

JSON-encoded user info, sent on the first sign-in only

Responses

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

Authenticate with a provider ID token

Exchanges an ID token that a native app obtained from the Google or Apple sign-in SDK for a session, creating the account when the identity is new. The session is set as cookies and the body returns the profile with is_new_user. The call must carry the calling device's access credential; the session binds to that device.

An identity linked to no account whose verified address already belongs to one is refused with 409 email_already_in_use.

path Parameters
provider
required
string
Enum: "google" "apple"
query Parameters
response_mode
string
Default: "cookie"
Enum: "cookie" "token"
Example: response_mode=token

Where the issued tokens go. cookie, the default, sets them as cookies for a browser; token returns them in the response body for a native or CLI client.

Request Body schema: application/json
required
id_token
required
string

ID token from the SSO provider

authorization_code
string

Authorization code the provider SDK returned next to the id_token. The server redeems it for the provider's refresh token, which it revokes when the account is deleted. Required for Sign in with Apple: without it the identity cannot be revoked at Apple on deletion.

display_name
string <= 255 characters

Display name for a new account

accept_terms
boolean
Deprecated
Default: false

Accepted and ignored. Signup is implicit for an unknown identity.

Responses

Request samples

Content type
application/json
{
  • "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ...",
  • "display_name": "John Doe",
  • "accept_terms": true
}

Response samples

Content type
application/json
{
  • "user": {
    },
  • "is_new_user": false
}

List authentication factors

Returns every authentication factor on the caller's account with its type and confirmation state, alongside available_types: the types the account may enroll now — what this deployment accepts, and email_code only while the account has none. A factor takes part in login once confirmed.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "available_types": [
    ]
}

Enroll an authentication factor

Creates a factor on the caller's account and returns what is needed to finish setup. A verified challenge in X-Auth-Challenge is required, unless this session confirmed another factor change within the deployment's step-up window.

The factor takes no part in login until it is confirmed. A password arrives confirmed, creating it being the proof, replaces the account's existing one and signs every other session of the account out — the one that set it stays; email_code arrives confirmed too, the address being the account's own. TOTP returns the shared secret and provisioning URI once, here; sms sends a code; webauthn returns registration options. Confirm those with POST /users/me/auth/factors/{factor_id}/confirm.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
type
required
string
Enum: "totp" "email_code" "sms" "password" "webauthn"

Factor type to create

phone_number
string

Phone number in E.164 format. Required when type is sms.

password
string

The password, 8 to 128 characters; required when type is password.

TOTPCreateMetadata (object) or EmailCodeCreateMetadata (object)

Type-specific parameters, for totp and email_code

Responses

Request samples

Content type
application/json
Example
{
  • "type": "totp",
  • "metadata": {
    }
}

Response samples

Content type
application/json
Example
{
  • "factor": {
    },
  • "enrollment": {
    }
}

Get an authentication factor

Returns one factor of the caller's own account: its type, confirmation state and what may be changed about it. A factor id belonging to another account reads as factor_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
factor_id
required
string
Example: totp_abc123def456xyz7

Public identifier of the factor

Responses

Response samples

Content type
application/json
{
  • "id": "totp_a1b2c3d4e5f6g7h8",
  • "type": "totp",
  • "is_confirmed": true,
  • "capabilities": {
    },
  • "created_at": "2025-05-20T08:00:00Z",
  • "last_used_at": "2025-05-21T10:20:00Z",
  • "metadata": { }
}

Remove an authentication factor

Deletes one of the caller's own factors. A verified challenge in X-Auth-Challenge is required; without one the call answers 403 challenge_required with a challenge bound to this deletion, unless this session confirmed another factor change within the deployment's step-up window with a factor other than this one.

The account's last sign-in method — password, passkey or email code — will not go (cannot_delete_last_sign_in_method). A deletion that leaves the account with a single confirmed factor while two-step verification is on switches it off, voids the recovery codes and says so in the response.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
factor_id
required
string
Example: totp_abc123def456xyz7

Public identifier of the factor to remove

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Responses

Response samples

Content type
application/json
{
  • "two_step_disabled": false
}

Confirm an enrolled factor

Proves a newly enrolled factor works and marks it confirmed. Send the code from the authenticator app, the email or the SMS as {"code": "123456"}, or the browser's registration result as webauthn_response. A password factor is confirmed when it is created, so it does not use this call.

A confirmed factor takes part in login from then on, at the step its type decides. A wrong credential answers invalid_code, and a factor that is already confirmed answers factor_already_confirmed.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
factor_id
required
string
Example: totp_abc123def456xyz7

Public identifier of the factor to confirm

Request Body schema: application/json

The proof the factor works: a code for totp, email_code and sms, the registration result for webauthn.

code
string [ 6 .. 8 ] characters

6 to 8 digit verification code, for totp, email_code and sms

object (WebAuthnAttestationResponse)

The PublicKeyCredential from navigator.credentials.create(), forwarded verbatim as the WebAuthn specification defines it.

Responses

Request samples

Content type
application/json
Example
{
  • "code": "123456"
}

Response samples

Content type
application/json
{
  • "factor": {
    },
  • "enrollment": {
    },
  • "access_token": "string"
}

Switch two-step verification on

Turns two-step verification on for the caller's account and returns its recovery codes, shown this once. From now on a sign-in by password or email code is followed by any other confirmed factor of the account; a passkey or SSO sign-in completes on its own.

Needs at least two confirmed factors (second_factor_required) and a verified challenge in X-Auth-Challenge, unless this session confirmed another factor change within the deployment's step-up window. Already on answers two_step_already_enabled.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "codes": [
    ],
  • "generated_at": "2025-05-20T08:00:00Z"
}

Switch two-step verification off

Turns two-step verification off and voids the recovery codes; the factors stay enrolled. A verified challenge in X-Auth-Challenge is required, unless this session confirmed another factor change within the deployment's step-up window. Already off answers two_step_not_enabled.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Recovery codes status

How many recovery codes are left unused. Zero, with no generated_at, while two-step verification is off.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "remaining": 7,
  • "generated_at": "2025-05-20T08:00:00Z"
}

Regenerate recovery codes

Issues a fresh set of recovery codes, shown this once, and voids the previous set. Needs two-step verification on (two_step_not_enabled) and a verified challenge in X-Auth-Challenge, unless this session confirmed another factor change within the last five minutes.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "codes": [
    ],
  • "generated_at": "2025-05-20T08:00:00Z"
}

Users

Account profile, email change, avatar, preferred region, devices and authentication factors.

Update the current user profile

Updates the caller's own profile and returns it in full. display_name renames the account. avatar carries a base64 data URL to upload a new image, an empty string to drop the uploaded one, and is left out to keep the current one. A request that changes neither is rejected.

The email address is changed through POST /users/me/email/change, not here. An image larger than 5 MB answers 413 avatar_too_large; an unsupported format or one outside the accepted pixel range answers 400.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
display_name
string [ 1 .. 255 ] characters
avatar
string or null

The image as a base64 data URL, in JPEG, PNG, WebP or GIF, at most 5 MB decoded and 64×64 to 4096×4096 pixels. An empty string deletes it; omitted or null keeps the current one.

language
string [ 1 .. 64 ] characters

The user's language as a tag, such as de or pt-BR. A value that is not a language tag answers 400. A language the server has no emails in is kept and mailed in English.

time_zone
string [ 1 .. 64 ] characters

IANA name of the user's time zone, such as Europe/Berlin. A name the IANA database does not hold answers 400.

Responses

Request samples

Content type
application/json
{
  • "display_name": "Patricia Manager"
}

Response samples

Content type
application/json
{
  • "user_id": "usr_pat1234567890123456",
  • "email": "pat@example.com",
  • "display_name": "Patricia Manager",
  • "avatar_source": "uploaded",
  • "avatar_updated_at": "2025-04-01T09:02:11Z",
  • "created_at": "2024-06-02T14:12:09Z",
  • "updated_at": "2025-04-01T09:02:11Z",
  • "two_step_enabled": false,
  • "workspaces": [
    ]
}

Delete the current user

Deletes the caller's account together with the workspaces it owns and everything anchored to them, and ends every session on every device. Irreversible.

Billing settles first: every subscription must already be inactive at the provider and no billing operation may be left pending, so scheduling a cancellation is not enough. An account that has not settled answers 409 billing_cancellation_required; a workspace the caller still owns and cannot take along answers 409 user_owns_workspaces, so hand it over before retrying.

An identity linked through Sign in with Apple is revoked at Apple before the account goes; if Apple refuses, the deletion fails and can be retried.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Get user avatar

Redirects to a short-lived signed URL of the user's uploaded avatar, served as uploaded (no server-side resize: size is accepted for forward compatibility and ignored). Any authenticated caller — a session or a device — may fetch any user's avatar by id: a user id is random and not guessable, and the avatar is what the user chose to be seen by. avatar_not_found when the user has no uploaded avatar (an unknown id reads the same). The avatar_url in the profile points here and carries a version parameter that changes with every upload, so a cached image is busted by the next one.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
user_id
required
string
Example: usr_pat1234567890123456

Public identifier of the user whose avatar is requested.

query Parameters
size
integer
Default: 128
Enum: 64 128 256

Requested pixel size. Accepted and ignored; the image is served as uploaded.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Get the preferred cloud region

Returns the caller's preferred cloud region for new personal links, or null when no preference is set and placement follows the caller's geography.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "preferred_region": "wasabi-eu"
}

Set the preferred cloud region

Sets the cloud region used for new personal links, overriding the geography-based choice, and returns the stored preference. null clears it and returns placement to geography. A region this deployment does not serve answers invalid_region.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
preferred_region
required
string or null

Cloud region for the user's new links. Null clears the preference and returns placement to geography.

Responses

Request samples

Content type
application/json
Example
{
  • "preferred_region": "wasabi-eu"
}

Response samples

Content type
application/json
{
  • "preferred_region": "wasabi-eu"
}

Unified device overview for the account

Returns the account's device inventory: one card per device, with the caller's live sessions on it, its Vault trust standing (trusted, pending or none) and whether it is online. A device is listed while it has a live session of the caller, a trust-circle membership or a pending join request. Another device's pending request is shown only to a caller whose device is in the trust circle.

The response also carries vault_state and, once a recovery key is stored, recovery. Read-only: revoking a session or trust and deciding a join request stay on their own endpoints. Requires a user session.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
Example

Four cards covering the full surface: the current trusted laptop with a live session; another machine's browser with a session but no trust standing; a phone asking to join (pending block with the request's snapshot); a trusted but logged-out device with no live sessions and minimal registration data.

{
  • "vault_state": "active",
  • "recovery": {
    },
  • "items": [
    ]
}

Request an email address change

Starts an email change: a 6-digit code goes to the new address, and the response says where it was sent and when it expires. Nothing changes on the account until POST /users/me/email/confirm verifies that code, before expires_at.

A verified challenge in X-Auth-Challenge is required; without one the call answers 403 challenge_required. An address already registered to another account answers email_already_in_use, and a change already awaiting confirmation answers email_change_pending.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Request Body schema: application/json
required
new_email
required
string <email> <= 320 characters

The address the account moves to once the code is confirmed.

Responses

Request samples

Content type
application/json
{
  • "new_email": "newemail@example.com"
}

Response samples

Content type
application/json
{
  • "status": "pending_verification",
  • "verification_sent_to": "newemail@example.com",
  • "expires_at": "2026-02-17T12:30:00Z"
}

Confirm an email address change

Confirms the pending email change with the 6-digit code sent to the new address. The account's address becomes the new one, every other session of the user is revoked while the calling one stays, and the response reports how many were ended.

A wrong code answers invalid_code and counts against the deployment's attempt limit, after which no further attempt is accepted (too_many_attempts). Past its window the change answers email_change_expired; with nothing pending, 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
code
required
string = 6 characters

The 6-digit code sent to the new address.

Responses

Request samples

Content type
application/json
{
  • "code": "123456"
}

Response samples

Content type
application/json
{
  • "email": "newemail@example.com",
  • "sessions_revoked": 3
}

Workspaces

Workspaces, invites, members and roles. Workspaces form a tree; every user gets one at signup. Members hold roles that grant permission scopes.

List the caller's workspaces

Returns every workspace the authenticated user created or joined, each with the user's role and effective permissions in it. Ordered by join date, oldest first.

Authorizations:
SessionCookieAuthAccessTokenAuth
query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

Create a workspace

Creates a workspace with the caller as its owner, holding the full workspace:* scope, and returns both the workspace and the caller's membership in it. Pass parent_workspace_id to nest the workspace under an existing one; omit it for a root-level workspace.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
name
required
string [ 1 .. 255 ] characters

Human-readable workspace name

description
string or null <= 1024 characters

What the workspace is for.

parent_workspace_id
string or null

Workspace to nest the new one under. Omit or null for a root workspace.

Responses

Request samples

Content type
application/json
{
  • "name": "Product Ops",
  • "description": "Shared space for the product operations crew"
}

Response samples

Content type
application/json
{
  • "workspace": {
    },
  • "membership": {
    }
}

Preview a workspace invite

Returns the public side of an invite so the user can see what they are joining before signing up: workspace name and description, the inviter's name, the role the invite grants and the current member count. No authentication is needed; the invite token itself is the authorization. The same preview as GET /invite/{token}, with the token in the query: an unknown or already redeemed token answers 404 invite_not_found, an expired or revoked one 410.

Authorizations:
SessionCookieAuthAccessTokenAuth
query Parameters
code
required
string [ 6 .. 128 ] characters
Example: code=t3Kc7QhX0v9mJd2sYw8LpN4uRbF6gHkA1eZiVoCxWqE

The invite token to preview

Responses

Response samples

Content type
application/json
{
  • "workspace_name": "Core Workspace",
  • "workspace_description": "Primary collaboration space for the core product group.",
  • "inviter_name": "Casey Ops",
  • "role": "member",
  • "member_count": 5
}

Preview a workspace invite

Returns the public metadata of a workspace invite: the workspace, the inviter, the role on offer and the current member count. The token is the authorization, so the holder can preview the invite without a session.

An invite is redeemed at signup only: the holder passes invite_token to POST /auth/sessions, and the account is created inside the workspace under the address the invite was sent to. An unknown or already redeemed token answers 404 invite_not_found, an expired or revoked one 410.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
token
required
string [ 6 .. 128 ] characters
Example: t3Kc7QhX0v9mJd2sYw8LpN4uRbF6gHkA1eZiVoCxWqE

Invite token taken from the invite link.

Responses

Response samples

Content type
application/json
{
  • "workspace_name": "Core Workspace",
  • "workspace_description": "Primary collaboration space for the core product group.",
  • "inviter_name": "Casey Ops",
  • "role": "member",
  • "member_count": 5
}

Accept a workspace invite

Adds the authenticated user to the invited workspace and returns the workspace with the resulting membership. A session or access token is required; the invite token alone is not enough.

Repeating the call for a user who already belongs answers 200 with status: already_member and changes nothing; a fresh join answers status: joined. A workspace at its 100-member cap answers 409. An invite that is expired, revoked or used up answers 410 with invite_expired, invite_revoked or invite_usage_exhausted.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
token
required
string [ 6 .. 128 ] characters
Example: inv_b7a3f5e9c2d14a9b8e7f6a3b2c1d0e9f

Invite token taken from the invite link.

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
Example
{
  • "status": "joined",
  • "workspace": {
    },
  • "membership": {
    }
}

Get workspace details

Returns one workspace. The caller must be a member; a workspace the caller cannot reach is answered 404, so its existence is never revealed.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace

Responses

Response samples

Content type
application/json
{
  • "workspace_id": "ws_7c3a1e5d-9f2b-4d6e-8a1c-3e5b9d7f2a4c",
  • "parent_workspace_id": null,
  • "name": "Core Workspace",
  • "description": "Primary collaboration space for the core product group.",
  • "inherit_parent_permissions": false,
  • "created_at": "2025-05-20T10:25:00Z",
  • "updated_at": "2025-05-20T10:25:00Z",
  • "preferred_region": "wasabi-eu"
}

Update workspace details

Changes the name, the description, or both. Requires the workspace:write scope or owner rights on the workspace. Returns the updated workspace.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace to update

Request Body schema: application/json
required
name
string [ 1 .. 255 ] characters

New name for the workspace

description
string or null <= 1024 characters

New description. Send null to clear it.

inherit_parent_permissions
boolean

When true, members inherit permissions from the parent workspace.

Responses

Request samples

Content type
application/json
{
  • "name": "Renamed Workspace",
  • "description": "Updated description"
}

Response samples

Content type
application/json
{
  • "workspace_id": "ws_7c3a1e5d-9f2b-4d6e-8a1c-3e5b9d7f2a4c",
  • "parent_workspace_id": null,
  • "name": "Core Workspace",
  • "description": "Primary collaboration space for the core product group.",
  • "inherit_parent_permissions": false,
  • "created_at": "2025-05-20T10:25:00Z",
  • "updated_at": "2025-05-20T10:25:00Z",
  • "preferred_region": "wasabi-eu"
}

Delete a workspace

Deletes a workspace and the data attached to it: every membership is removed, every invite it issued stops working, and its resources are cleaned up. Requires owner rights (workspace:* or the workspace:own scope). The caller's last remaining workspace cannot be deleted. A workspace that still has child workspaces, active links or billing descendants answers 409.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace to delete

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Get the workspace preferred region

Returns the workspace's preferred cloud region. While it is set, new links in the workspace use it instead of the region chosen from the caller's location.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_9f63b7a0d6f54cb5

Workspace public ID

Responses

Response samples

Content type
application/json
{
  • "preferred_region": "wasabi-eu"
}

Set the workspace preferred region

Sets the cloud region new links in the workspace use, overriding the region chosen from the caller's location. Send preferred_region: null to clear it and go back to that default. Requires the workspace:write scope or owner rights. An unknown region name answers 400 invalid_region.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_9f63b7a0d6f54cb5

Workspace public ID

Request Body schema: application/json
required
preferred_region
required
string or null

Cloud region for the user's new links. Null clears the preference and returns placement to geography.

Responses

Request samples

Content type
application/json
Example
{
  • "preferred_region": "wasabi-eu"
}

Response samples

Content type
application/json
{
  • "preferred_region": "wasabi-eu"
}

List workspace invites

Returns the workspace's live invites: id, the address each was sent to, the role it grants when redeemed and its expiry. The invite token itself travels only in the mail; reads show its tail as code_hint.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f

Identifier of the workspace whose invites should be returned

query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

Create a workspace invite

Invites one person, by address, to create an account in the workspace. The invite is mailed to recipient_email and redeemed once, at signup with its token; the account is created under that address, whatever the recipient types. An address that already has an account answers 409 email_already_in_use: an account belongs to its own workspace and cannot be added to another.

The invite's role is what the new member gets, and only an owner can create an owner-level invite, otherwise the answer is 403. expires_at sets when the invite stops working; omitted, the server's default lifetime applies.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f

Identifier of the workspace for which the invite will be created

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
recipient_email
required
string <email>

Address to mail the invite to; the account is created under it.

role
string or null

Role the invite grants, one of the application's roles; omitted, the application's default.

expires_at
string or null <date-time>

When the invite stops working. Omitted, the server's default lifetime applies.

Responses

Request samples

Content type
application/json
{
  • "recipient_email": "casey@example.com",
  • "role": "member",
  • "expires_at": "2025-06-20T09:00:00Z"
}

Response samples

Content type
application/json
{
  • "invite_id": "inv_018f7c41-5a2b-7c3d-8e9f-1a2b3c4d5e6f",
  • "workspace_id": "ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f",
  • "code_hint": "...OPS",
  • "recipient_email": "casey@example.com",
  • "role": "member",
  • "effective_permissions": [
    ],
  • "created_by_user_id": "usr_6f1a9c3e-8b2d-4f7a-9c1e-5d3b7a2f8e4c",
  • "created_at": "2025-05-20T09:00:00Z",
  • "expires_at": "2025-06-20T09:00:00Z"
}

Revoke a workspace invite

Revokes an invite. Its token stops working immediately, so the recipient can no longer sign up with it.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f

Identifier of the workspace that issued the invite

invite_id
required
string
Example: inv_018f7c41-5a2b-7c3d-8e9f-1a2b3c4d5e6f

Identifier of the invite to revoke

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

List workspace members

Returns the workspace's members, each with their id, email and display name, their role and effective permissions, the date they joined and who invited them. Ordered by join date. Requires at least the workspace:read scope.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace whose members should be returned

query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

Add a workspace member

Adds a known user to the workspace directly, without an invite, and returns the new membership. The role granted cannot exceed the caller's own. A user who is already a member answers 409 user_already_workspace_member.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace to which the member will be added

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
user_id
required
string

User to add to the workspace

role
string or null
Enum: "member" "admin" "owner"

Role to give the user. Defaults to member.

cascade_to_children
boolean

When true, the member also gains access to every child workspace.

send_welcome_email
boolean

When true, the new member receives a welcome message.

Responses

Request samples

Content type
application/json
{
  • "user_id": "usr_a1b2c3d4e5f6g7h8i9j0",
  • "role": "member",
  • "send_welcome_email": true
}

Response samples

Content type
application/json
{
  • "user_id": "usr_casey123456789012345",
  • "email": "casey@example.com",
  • "display_name": "Casey Ops",
  • "role": "member",
  • "effective_permissions": [
    ],
  • "joined_at": "2025-05-21T09:30:00Z",
  • "invited_by_user_id": "usr_pat1234567890123456",
  • "cascade_to_children": false
}

Get workspace member statistics

Returns per-member activity for the workspace: links created, upload and download counts and storage used. Only links owned by this workspace are counted. Requires the workspace:read scope or owner rights.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Get a workspace member

Returns one member of the workspace with their role and effective permissions. The caller must be a member of the workspace.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace

user_id
required
string
Example: usr_a1b2c3d4e5f6g7h8i9j0

Identifier of the user whose membership to retrieve

Responses

Response samples

Content type
application/json
{
  • "user_id": "usr_a4e7c2d9-3b1f-4e6a-8d5c-2f9b7e1a6c3d",
  • "email": "casey@example.com",
  • "display_name": "Casey Ops",
  • "role": "member",
  • "effective_permissions": [
    ],
  • "joined_at": "2024-08-15T10:45:00Z",
  • "invited_by_user_id": "usr_c8d2f5a1-7e3b-4c9d-a6f2-1b4e8d7c5a9f",
  • "cascade_to_children": false,
  • "identity_public_key": "MCowBQYDK2VuAyEAq8Gk1GMnfN3WjLKBHaOvhB0tXRuQbVJLBqFz5mFqHQA="
}

Update a workspace member

Changes a member's role (member, admin or owner); the result is returned as the updated membership. A member's permissions are their role's — there are no per-member extras.

Requires the workspace:write scope or owner rights. The caller cannot grant more than they hold themselves, and only an owner may change another owner. The workspace must keep at least one owner, so downgrading the last one answers 403 workspace_must_have_at_least_one_owner.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace that contains the member

user_id
required
string
Example: usr_a1b2c3d4e5f6g7h8i9j0

Identifier of the user whose membership will be updated

Request Body schema: application/json
required
role
required
string

Role to give the user; the application's role names.

cascade_to_children
boolean

When true, the member's permissions also apply in every child workspace.

Responses

Request samples

Content type
application/json
{
  • "role": "member"
}

Response samples

Content type
application/json
{
  • "user_id": "usr_a4e7c2d9-3b1f-4e6a-8d5c-2f9b7e1a6c3d",
  • "email": "casey@example.com",
  • "display_name": "Casey Ops",
  • "role": "member",
  • "effective_permissions": [
    ],
  • "joined_at": "2024-08-15T10:45:00Z",
  • "invited_by_user_id": "usr_c8d2f5a1-7e3b-4c9d-a6f2-1b4e8d7c5a9f",
  • "cascade_to_children": false,
  • "identity_public_key": "MCowBQYDK2VuAyEAq8Gk1GMnfN3WjLKBHaOvhB0tXRuQbVJLBqFz5mFqHQA="
}

Remove a workspace member

Removes a user from the workspace; their access ends immediately. Requires the workspace:write scope or owner rights, except that a member may always remove themselves. Only an owner may remove another owner, and the last owner cannot be removed — hand ownership over first.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the workspace that contains the member

user_id
required
string
Example: usr_a1b2c3d4e5f6g7h8i9j0

Identifier of the user whose membership will be removed

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

List workspace roles

Returns the roles a workspace offers: the preset ones (owner, admin, member), which cannot be changed or deleted, and any custom roles defined for this workspace with their own permission sets.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f

Workspace public ID

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Create a custom role

Creates a custom role in the workspace and returns it. role_key must match [a-z][a-z0-9_]* and be unique within the workspace.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string

Workspace public ID

Request Body schema: application/json
required
role_key
required
string^[a-z][a-z0-9_]*$

Unique identifier for the role within the workspace

display_name
required
string

Human-readable name

permissions
required
Array of strings

Permission scopes granted to this role

Responses

Request samples

Content type
application/json
{
  • "role_key": "reviewer",
  • "display_name": "Code Reviewer",
  • "permissions": [
    ]
}

Response samples

Content type
application/json
{
  • "role_id": "role_123",
  • "workspace_id": "ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f",
  • "role_key": "reviewer",
  • "display_name": "Code Reviewer",
  • "permissions": [
    ],
  • "is_preset": false,
  • "created_at": "2025-01-15T10:30:00Z"
}

Get role details

Returns one role of the workspace with its permission set.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string

Workspace public ID

role_id
required
string
Example: role_123

Role ID (role_{id} format)

Responses

Response samples

Content type
application/json
{
  • "role_id": "role_123",
  • "workspace_id": "ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f",
  • "role_key": "reviewer",
  • "display_name": "Code Reviewer",
  • "permissions": [
    ],
  • "is_preset": false,
  • "created_at": "2025-01-15T10:30:00Z"
}

Update a custom role

Changes a custom role's display name or replaces its permission set. Preset roles cannot be changed.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string

Workspace public ID

role_id
required
string

Role ID

Request Body schema: application/json
required
display_name
string

New display name

permissions
Array of strings

New permission set (replaces existing)

Responses

Request samples

Content type
application/json
{
  • "display_name": "string",
  • "permissions": [
    ]
}

Response samples

Content type
application/json
{
  • "role_id": "role_123",
  • "workspace_id": "ws_2b9e4f7a-1c3d-4a8b-9f6e-7d2c5b1a3e9f",
  • "role_key": "reviewer",
  • "display_name": "Code Reviewer",
  • "permissions": [
    ],
  • "is_preset": false,
  • "created_at": "2025-01-15T10:30:00Z"
}

Delete a custom role

Deletes a custom role; members holding it lose the permissions it carried. Preset roles cannot be deleted.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string

Workspace public ID

role_id
required
string

Role ID

Responses

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

List child workspaces

Returns the workspaces nested directly under the given one. Only immediate children come back; walk deeper by calling this again for each child.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Identifier of the parent workspace whose children should be listed

query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

Get workspace usage

Returns the workspace's current usage counters: storage, transfer bytes this month and active links. The counters cover the whole billing subtree the workspace belongs to, while storage_bytes_workspace reports this workspace alone. Add ?breakdown=true for per-workspace storage across that subtree. Requires the workspace:read permission; a workspace the caller cannot reach is answered 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_coreworkspace1

Workspace identifier

query Parameters
breakdown
boolean
Default: false
Example: breakdown=true

When true, the response also carries breakdown_by_child: storage per workspace across the billing subtree.

Responses

Response samples

Content type
application/json
{
  • "principal_id": "ws_coreworkspace1",
  • "counters": {
    },
  • "storage_bytes_workspace": 5368709120,
  • "updated_at": "2025-02-03T10:30:00Z"
}

Get workspace limits

Returns the limits in force for the workspace once tier defaults, workspace overrides and feature flags have been applied, together with the tier it sits on and how many overrides it carries. A limit of null means unlimited. Requires the workspace:read permission; a workspace the caller cannot reach is answered 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_coreworkspace1

Workspace identifier

Responses

Response samples

Content type
application/json
{
  • "limits": {
    },
  • "tier_slug": "enterprise",
  • "tier_name": "Enterprise Plan",
  • "override_count": 1,
  • "features": {
    }
}

Get the workspace avatar

Redirects to the workspace's avatar image in one of the sizes 64, 128 or 256 pixels, 128 by default. A workspace with no uploaded avatar answers 404. The response carries cache headers, and the redirect URL changes whenever the avatar does, so a cached copy is never served for a new image.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_coreworkspace1

Workspace identifier

query Parameters
size
integer
Default: 128
Enum: 64 128 256

Avatar size in pixels

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Upload the workspace avatar

Uploads or replaces the workspace's avatar, sent as a base64 data URL, and returns the updated workspace. Accepted formats are JPEG, PNG, WebP and GIF; the decoded image may not exceed the deployment's size cap, 5 MB by default. Requires the workspace:write scope.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_coreworkspace1

Workspace identifier

Request Body schema: application/json
required
avatar
required
string

The image as a data URL, data:image/<type>;base64,<data>, where type is jpeg, png, webp or gif.

Responses

Request samples

Content type
application/json
{
  • "avatar": "data:image/png;base64,iVBORw0KGgo..."
}

Response samples

Content type
application/json
{
  • "workspace_id": "ws_7c3a1e5d-9f2b-4d6e-8a1c-3e5b9d7f2a4c",
  • "parent_workspace_id": null,
  • "name": "Core Workspace",
  • "description": "Primary collaboration space for the core product group.",
  • "inherit_parent_permissions": false,
  • "created_at": "2025-05-20T10:25:00Z",
  • "updated_at": "2025-05-20T10:25:00Z",
  • "preferred_region": "wasabi-eu"
}

Delete the workspace avatar

Removes the workspace's avatar. Requires the workspace:write scope.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_coreworkspace1

Workspace identifier

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Vault

End-to-end encryption keys: the identity key pair, the trust circle of devices, share keys wrapped per recipient, recovery, and claims for anonymous sharing. Clients read GET /vault/config on startup for the active cipher suite.

Get vault crypto configuration

Returns the cipher suite configuration for vault end-to-end encryption. Clients read it on startup and follow three rules: encrypt new blobs with latest_suite_version, decrypt any suite they know, and treat an unknown version as a hard error rather than falling back.

Authorizations:
AccessTokenAuthSessionCookieAuth

Responses

Response samples

Content type
application/json
{
  • "min_suite_version": 1,
  • "latest_suite_version": 1
}

List trusted devices Deprecated

Superseded by GET /users/me/devices, where trusted devices are the cards with trust.status: trusted. Returns the devices in the caller's trust circle. Only a device inside the circle may read the roster.

Authorizations:
AccessTokenAuthSessionCookieAuth

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Register first device

Registers the calling device as the first device of the trust circle and stores the user's identity keypair. Needs a login session, not trust circle membership. A user that already has an identity key answers 409 vault_already_initialized.

Authorizations:
AccessTokenAuthSessionCookieAuth
Request Body schema: application/json
required
device_public_key
required
string <byte> <= 64 characters

X25519 device public key (base64) — exactly 32 bytes after decoding

encrypted_identity_key
required
string <byte> <= 1024 characters

Identity private key wrapped to the device public key (hybrid encryption). A blob under 61 decoded bytes — suite byte, ephemeral public key, IV and GCM tag — is refused as validation_failed.

identity_public_key
required
string <byte> <= 64 characters

Identity X25519 public key (base64) — exactly 32 bytes after decoding

encrypted_identity_key_for_recovery
string <byte> <= 1024 characters

Identity private key encrypted with the recovery master key. Optional; can be set later with PUT /vault/recovery. Under 28 decoded bytes (IV and GCM tag) it is refused as validation_failed.

Responses

Request samples

Content type
application/json
{
  • "device_public_key": "string",
  • "encrypted_identity_key": "string",
  • "identity_public_key": "string",
  • "encrypted_identity_key_for_recovery": "string"
}

Response samples

Content type
application/json
{
  • "agent_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "device_name": "MacBook Pro",
  • "device_fingerprint": "a1b2c3d4",
  • "device_public_key": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "last_seen_at": "2019-08-24T14:15:22Z",
  • "is_current": true,
  • "display_name": "string",
  • "device": {
    }
}

Request to join trust circle

Submits a join request from the calling device. No body: the device's public key and name are the calling agent's own, already known from its registration. Needs a login session, not trust circle membership; the vault must already be initialized. Every trusted device receives a vault.device_join_requested frame, and the response lists the same devices as approvers, so the asking device can show where to confirm.

Asking again is safe and is the way out of a stale ask: the account keeps one live request per device, so a repeat returns the standing request with the same request_id and the deadline pushed out. A repeat also drops the sign_in_peer_agent_id mark of a request the server opened for a consent browser, so a request only the signed-in device was shown reaches the whole circle. A device that switched accounts asks each vault independently.

Authorizations:
AccessTokenAuthSessionCookieAuth

Responses

Response samples

Content type
application/json
{
  • "request_id": "string",
  • "device_fingerprint": "string",
  • "status": "pending",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "approvers": [
    ]
}

List pending join requests Deprecated

Superseded by GET /users/me/devices, where pending requests are the cards with trust.status: pending, carrying the request snapshot and the device key needed to approve. Returns the join requests awaiting a decision in the caller's trust circle. Only a device inside the circle may read them and decide them.

Authorizations:
AccessTokenAuthSessionCookieAuth

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Approve device join request

Approves a pending join request. The caller must itself be a trusted device and supplies encrypted_identity_key — the identity private key wrapped to the requesting device's public key. The requester receives vault.device_approved carrying that wrap; the remaining trusted devices receive vault.device_approved_broadcast so their device lists refresh.

A request another trusted device already decided answers 409 join_request_already_decided; one already rejected or expired answers 410.

Authorizations:
AccessTokenAuthSessionCookieAuth
path Parameters
agent_id
required
string (AgentID) [ 8 .. 256 ] characters ^agt_[A-Za-z0-9_-]+$
Example: agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Unique agent identifier, prefixed with agt_. Returned by POST /auth/agents as agent_id and carried in the access token's claims, so no separate header is needed.

Request Body schema: application/json
required
encrypted_identity_key
required
string <byte> <= 1024 characters

Identity private key wrapped to the new device's public key (hybrid encryption). Under the 61-decoded-byte floor it is refused as validation_failed.

Responses

Request samples

Content type
application/json
{
  • "encrypted_identity_key": "string"
}

Response samples

Content type
application/json
{
  • "agent_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "device_name": "MacBook Pro",
  • "device_fingerprint": "a1b2c3d4",
  • "device_public_key": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "last_seen_at": "2019-08-24T14:15:22Z",
  • "is_current": true,
  • "display_name": "string",
  • "device": {
    }
}

Reject device join request

Rejects a pending join request. The caller must itself be a trusted device. The rejected device receives vault.device_rejected by direct push; the user's trusted devices receive vault.device_rejected_broadcast so their pending lists drop the request. The optional reason travels only to the rejected device, as written, and is recorded in the audit log.

Authorizations:
AccessTokenAuthSessionCookieAuth
path Parameters
agent_id
required
string (AgentID) [ 8 .. 256 ] characters ^agt_[A-Za-z0-9_-]+$
Example: agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Unique agent identifier, prefixed with agt_. Returned by POST /auth/agents as agent_id and carried in the access token's claims, so no separate header is needed.

Request Body schema: application/json
optional
reason
string [ 1 .. 500 ] characters

Free-text rejection reason chosen by the rejecting device's user.

Responses

Request samples

Content type
application/json
{
  • "reason": "string"
}

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

Revoke device from trust circle

Removes a device from the trust circle and deletes its wrapped copy of the identity key. The revoked device receives vault.device_self_revoked, loses its subscriptions, its sessions and its socket; every remaining trusted device receives vault.device_revoked. The caller must itself be a trusted device and cannot revoke the one holding the current session (409 cannot_revoke_current_device).

Authorizations:
AccessTokenAuthSessionCookieAuth
path Parameters
agent_id
required
string (AgentID) [ 8 .. 256 ] characters ^agt_[A-Za-z0-9_-]+$
Example: agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Unique agent identifier, prefixed with agt_. Returned by POST /auth/agents as agent_id and carried in the access token's claims, so no separate header is needed.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Get recovery key status

Reports whether a recovery-encrypted identity key exists for the user. Never returns the blob itself — that is POST /vault/recovery/recover.

Any login session of the user may ask, trusted or not: a device that lost the trust circle needs this answer to know whether recovery is an option or reset is the only way back.

Authorizations:
AccessTokenAuthSessionCookieAuth

Responses

Response samples

Content type
application/json
{
  • "configured": true,
  • "updated_at": "2019-08-24T14:15:22Z"
}

Upsert recovery key

Stores the recovery-encrypted identity key, replacing any existing one for the caller. Requires trust circle membership. The same payload covers both first setup and regeneration; the caller does not need to know which case applies.

Authorizations:
AccessTokenAuthSessionCookieAuth
Request Body schema: application/json
required
encrypted_identity_key_for_recovery
required
string <byte> <= 1024 characters

Identity private key encrypted with the recovery master key (symmetric AES-256-GCM). Under 28 decoded bytes (IV and GCM tag) it is refused as validation_failed.

Responses

Request samples

Content type
application/json
{
  • "encrypted_identity_key_for_recovery": "string"
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Start vault recovery

Returns the recovery-encrypted identity key. Needs a login session, not trust circle membership. Decrypt it with the recovery key, re-encrypt the identity private key under the new device's public key, then call POST /vault/recovery/complete to finish. A user that never stored a recovery key answers 404 recovery_not_set_up.

Authorizations:
AccessTokenAuthSessionCookieAuth
Request Body schema: application/json
required
device_public_key
required
string <byte> <= 64 characters

New device's X25519 public key — exactly 32 bytes after decoding

Responses

Request samples

Content type
application/json
{
  • "device_public_key": "string"
}

Response samples

Content type
application/json
{
  • "encrypted_identity_key_for_recovery": "string"
}

Complete vault recovery

Finishes the flow started by POST /vault/recovery/recover: submit the identity key decrypted with the recovery words and re-encrypted under the new device's public key. The submitted identity_public_key must match the user's stored one; every device in the circle is then revoked, losing its sessions and sockets, and the calling device is enrolled as the sole trusted one. The identity is unchanged, so wrapped share keys survive.

Needs a login session and a fresh factor in X-Auth-Challenge, not trust circle membership. A caller already in the circle answers 409 device_already_trusted.

Authorizations:
AccessTokenAuthSessionCookieAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Request Body schema: application/json
required
device_public_key
required
string <byte> <= 64 characters

X25519 public key of the new device (base64) — exactly 32 bytes after decoding.

encrypted_identity_key
required
string <byte> <= 1024 characters

Identity private key, decrypted with the recovery words and re-encrypted under device_public_key (base64); under the 61-decoded-byte floor it is refused as validation_failed.

identity_public_key
required
string <byte> <= 64 characters

Identity X25519 public key — exactly 32 bytes after decoding. Must match the user's stored one; it binds the call to the right vault.

Responses

Request samples

Content type
application/json
{
  • "device_public_key": "string",
  • "encrypted_identity_key": "string",
  • "identity_public_key": "string"
}

Response samples

Content type
application/json
{
  • "agent_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "device_name": "MacBook Pro",
  • "device_fingerprint": "a1b2c3d4",
  • "device_public_key": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "last_seen_at": "2019-08-24T14:15:22Z",
  • "is_current": true,
  • "display_name": "string",
  • "device": {
    }
}

Reset the vault

The way out when both the trust circle and the recovery key are lost. One transaction swaps in the submitted identity, empties the circle, discards the caller's wrapped share keys, deletes every link the account owns, replaces the recovery key with the submitted wrap or deletes it when none is sent, and enrols the caller as the sole trusted device; the replaced devices lose their sessions and sockets.

Each deleted link is taken down like any other: subscribers hear link.deleted, and its recipients lose access. Links a device created before signing in belong to the device and stay. Grants and workspace membership survive, so the holder of a link someone else shares with the caller can re-wrap its key through the pending-key bridge.

Needs a fresh factor in X-Auth-Challenge; re-submitting the stored identity_public_key is refused as validation_failed.

Authorizations:
AccessTokenAuthSessionCookieAuth
header Parameters
X-Auth-Challenge
string [ 16 .. 64 ] characters
Example: ch_wjIilzM03bjfJhYdEPkGN9CAZdlx_AO5

Challenge token verified with POST /auth/challenges/{token}, sent when retrying an operation that answered 403 challenge_required — changing, deleting or confirming a factor, among others. Single use, until its expires_at.

Request Body schema: application/json
required
device_public_key
required
string <byte> <= 64 characters

X25519 public key of the new device (base64) — exactly 32 bytes after decoding.

identity_public_key
required
string <byte> <= 64 characters

New identity X25519 public key (base64) — exactly 32 bytes after decoding. Must differ from the stored one, which is device recovery rather than a reset.

encrypted_identity_key
required
string <byte> <= 1024 characters

New identity private key wrapped to device_public_key (base64); under the 61-decoded-byte floor it is refused as validation_failed.

encrypted_identity_key_for_recovery
string <byte> <= 1024 characters

New identity private key encrypted with a new recovery master key (base64). Optional: without it the stored recovery key is deleted. Under 28 decoded bytes it is refused as validation_failed.

Responses

Request samples

Content type
application/json
{
  • "device_public_key": "string",
  • "identity_public_key": "string",
  • "encrypted_identity_key": "string",
  • "encrypted_identity_key_for_recovery": "string"
}

Response samples

Content type
application/json
{
  • "agent_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "device_name": "MacBook Pro",
  • "device_fingerprint": "a1b2c3d4",
  • "device_public_key": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "last_seen_at": "2019-08-24T14:15:22Z",
  • "is_current": true,
  • "display_name": "string",
  • "device": {
    }
}

Deliver wrapped share keys

Writes wrapped share keys into the vault key-store for one or more (link, recipient) pairs — all rows or none. Key delivery only: no scopes and no ACL changes. Any key holder able to read the link may deliver, and delivery never widens access: a row is written only for a recipient that already reaches the link, and a keyless target answers 404 pending_key_not_found.

The key-store is insert-only. The first delivery for a pair wins, a replay with identical bytes counts as delivered, and different bytes answer 409 vault_key_conflict and roll the whole batch back.

Authorizations:
AccessTokenAuthSessionCookieAuth
Request Body schema: application/json
required
required
Array of objects (VaultKeyBatchItem) [ 1 .. 100 ] items

Responses

Request samples

Content type
application/json
{
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "delivered": 0
}

Reset own wrapped share key

Deletes the caller's own key-store row for the link and re-queues the caller as authorized-but-keyless, so an online key holder delivers a fresh wrap. This is the repair path for a key that fails to decrypt. Only the caller's own row can be reset; another recipient's key is not reachable through any endpoint.

Authorizations:
AccessTokenAuthSessionCookieAuth
path Parameters
url_token
required
string

Link URL token

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

List pending share key requests

Returns the pending key requests for links the caller holds a key for, as owner or as a recipient with a key-store row — the catch-up companion to the vault.share_key_needed frame, for a holder that was offline when the request was queued. Fulfil them with POST /vault/keys/batch.

Authorizations:
AccessTokenAuthSessionCookieAuth
query Parameters
workspace_id
string

Optional filter: only pending keys for links owned by this workspace. Omitted → all pending keys the caller can fulfil.

cursor
string

Cursor from previous response for pagination

limit
integer [ 1 .. 100 ]
Default: 100

Maximum number of items to return

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "cursor": "string"
}

Create claim token for anonymous sharing

Attaches a claim to a link, so that whoever holds the raw claim token can unwrap the link's share key. The client generates the claim token locally, derives an encryption key from it with HKDF, encrypts the share key with that, and sends only claim_token_hash and encrypted_share_key; the server never sees the raw token.

An agent session is enough — no user login is required. The server checks that url_token exists and caps expires_at at 30 days.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
url_token
required
string

URL token of the link to attach the claim to

claim_token_hash
required
string <byte> [ 1 .. 64 ] characters

SHA256(claim_token), base64-encoded — the server stores only the hash

encrypted_share_key
required
string <byte> [ 1 .. 1024 ] characters

Share key encrypted with the AES key derived from HKDF(claim_token), base64

expires_at
string <date-time>

Requested expiry. Capped at 30 days from now; absent means the cap.

Responses

Request samples

Content type
application/json
{
  • "url_token": "k7mstq3w",
  • "claim_token_hash": "string",
  • "encrypted_share_key": "string",
  • "expires_at": "2019-08-24T14:15:22Z"
}

Response samples

Content type
application/json
{
  • "expires_at": "2019-08-24T14:15:22Z"
}

Get share by claim token hash

Returns the claim's encrypted share key. The client sends SHA256 of its raw claim token as the path parameter and decrypts the returned key locally with HKDF(claim_token); the server only ever sees the hash. An agent session is enough — no user login is required. An expired or unknown claim answers 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
claim_token_hash
required
string [ 1 .. 64 ] characters

SHA256(claim_token), base64url-encoded (the server base64url-decodes it)

Responses

Response samples

Content type
application/json
{
  • "url_token": "string",
  • "encrypted_share_key": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "expires_at": "2019-08-24T14:15:22Z"
}

Bind anonymous claim to logged-in user

Turns an anonymous claim into a permanent grant for the calling user: the client re-encrypts the share key under its own identity public key, and the server writes the read grant and the key-store row together. Requires a login session and an initialized vault.

The claim is spent by the call — only an unclaimed, unexpired claim can be bound, and one already bound answers 409 claim_already_exists.

Authorizations:
AccessTokenAuthSessionCookieAuth
path Parameters
claim_token_hash
required
string [ 1 .. 64 ] characters

SHA256(claim_token), base64url-encoded (the server base64url-decodes it)

Request Body schema: application/json
required
encrypted_share_key
required
string <byte> <= 1024 characters

Share key re-encrypted under the user's identity public key (base64)

Responses

Request samples

Content type
application/json
{
  • "encrypted_share_key": "string"
}

Response samples

Content type
application/json
{
  • "url_token": "string",
  • "status": "claimed"
}

Create a link

Creates an ephemeral link with the datasources it asks for: p2p_storage for direct WebRTC transfer between agents, cloud_storage for object storage reached through per-file presigned URLs.

A registered caller passes workspace_id, which decides the storage pool, billing and admin controls; an anonymous agent omits it and owns the link on the Anonymous tier.

validity is the lifetime in seconds, 0 meaning no expiry; on a cloud link it is clamped to the owner tier's retention, which an omitted validity takes. 206 means a requested datasource was not provisioned.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
workspace_id
string

Public ID of the workspace that owns the link: its storage pool, billing, ACL-targetable members and admin controls. An anonymous agent omits it and owns the link itself.

description
string <= 1024 characters

Share description

validity
integer >= 0

Link validity in seconds; 0 means the link never expires. Capped at the owner tier's retention maximum; absent, the link gets that maximum.

cloud_storage
boolean
Default: false

Provision cloud storage for the link

p2p_storage
boolean
Default: true

Allow direct P2P transfer for the link

instance_id
string^[-a-zA-Z0-9_]{1,64}$

Runtime instance that serves this link's P2P bytes, as registered with POST /auth/agents. P2P availability then follows that instance rather than the whole agent. Requires p2p_storage: true.

public
boolean
Default: true

Let anyone holding the link download from it

provider
string

Cloud provider alias

estimated_bytes
integer <int64> >= 0

Declared total upload size in bytes for the link, surfaced as Link.size.estimated_bytes. The owner's storage quota is checked against it; a link without it issues no cloud upload access.

encrypted_metadata
string <byte> <= 65536 characters

Opaque base64 blob encrypted by the client with the link share key. Stored as-is and never interpreted by the server.

encrypted_share_key
string <byte> <= 1024 characters

The link's share key wrapped with the owner's identity public key, base64; it comes back on GET /links/{url_token}. Optional — the owner may deliver its own key later with POST /vault/keys/batch.

download_limit
integer <int64> >= 1

Ceiling on download sessions for the link; 1 makes it single-use. Paid tiers only: a Free or Anonymous owner answers 402 link_options_upgrade_required.

notify_on_open
boolean
Default: false

Email the owner on every download session start. Paid tiers only: a Free or Anonymous owner answers 402 link_options_upgrade_required.

notify_on_download
boolean
Default: false

Email the owner on every file downloaded (a successful file-transfer end in a download session). Paid tiers only: a Free or Anonymous owner answers 402 link_options_upgrade_required.

Responses

Request samples

Content type
application/json
{
  • "workspace_id": "ws_abc123",
  • "description": "string",
  • "validity": 3600,
  • "cloud_storage": false,
  • "p2p_storage": true,
  • "instance_id": "tab-9f2c4b1a",
  • "public": true,
  • "provider": "wasabi",
  • "estimated_bytes": 104857600,
  • "encrypted_metadata": "string",
  • "encrypted_share_key": "string",
  • "download_limit": 1,
  • "notify_on_open": false,
  • "notify_on_download": false
}

Response samples

Content type
application/json
{
  • "url_token": "oth5mu1D",
  • "url": "/oth5mu1D/",
  • "retention_clamped": true,
  • "requested_retention_sec": 0,
  • "max_allowed_retention_sec": 0
}

Get link details

Returns the link's metadata, its datasources and the caller's effective_permissions. Requires link:read, held through ownership or an ACL grant. The write token is returned to the link owner only.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string
Example: oth5mu1D

Link URL token

query Parameters
claim_token_hash
string [ 1 .. 64 ] characters

SHA256 of a claim token, base64url-encoded; the server never sees the raw token.

When the claim refers to this link, encrypted_share_key comes from the claim, wrapped with HKDF(claim_token) rather than the caller's identity public key, which saves a call to GET /vault/claims/{claim_token_hash}. It grants no access: a caller without link:read still gets 403 or 404.

A claim that is unknown, expired or refers to another link is ignored, and encrypted_share_key falls back to the caller's own grant or is absent.

Responses

Response samples

Content type
application/json
{
  • "url_token": "oth5mu1D",
  • "url": "/oth5mu1D/",
  • "description": "string",
  • "encrypted_metadata": "string",
  • "encrypted_share_key": "string",
  • "user": {
    },
  • "agent": {
    },
  • "workspace_id": "ws_a1b2c3d4e5f6g7h8",
  • "effective_permissions": [
    ],
  • "created_at": "2024-01-15T10:30:00Z",
  • "invalidate_at": "2024-01-16T10:30:00Z",
  • "deleted_at": "2024-01-17T10:30:00Z",
  • "size": {
    },
  • "download_count": 42,
  • "download_limit": 1,
  • "notify_on_open": true,
  • "notify_on_download": true,
  • "cloud": true,
  • "p2p": false
}

Update link settings

Updates the link's description, encrypted metadata, validity, declared size or datasources. Requires link:manage.

Datasource flags are additive: cloud_storage: true adds a cloud datasource when the link has none, and no datasource is ever removed. Setting validity restarts the lifetime from now.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
description
string <= 1024 characters
encrypted_metadata
string <byte> <= 65536 characters

Opaque base64 blob encrypted by the client with the link share key. Stored as-is and never interpreted by the server.

validity
integer >= 0

Link validity in seconds from now; 0 means the link never expires. Capped at the link's created_at plus the owner tier's retention maximum, so a patch cannot extend a link past the window it was born with.

cloud_storage
boolean
p2p_storage
boolean
provider
string
estimated_bytes
integer <int64> >= 0

Re-declared total upload size in bytes. Checked against the owner's storage cap with the link's own stored bytes excluded; a declaration past the remainder answers 409 storage_quota_exceeded.

download_limit
integer <int64> >= 0

Ceiling on download sessions; 0 removes the limit. Setting a limit needs a paid tier on the owner, else 402 link_options_upgrade_required; removing one never does. A limit at or under download_count closes the link to new downloads at once.

notify_on_open
boolean

Email the owner on every download session start. Turning it on needs a paid tier on the owner, else 402 link_options_upgrade_required; turning it off never does.

notify_on_download
boolean

Email the owner on every file downloaded (a successful file-transfer end in a download session). Turning it on needs a paid tier on the owner, else 402 link_options_upgrade_required; turning it off never does.

Responses

Request samples

Content type
application/json
{
  • "description": "string",
  • "encrypted_metadata": "string",
  • "validity": 0,
  • "cloud_storage": true,
  • "p2p_storage": true,
  • "provider": "string",
  • "estimated_bytes": 104857600,
  • "download_limit": 1,
  • "notify_on_open": true,
  • "notify_on_download": true
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Delete a link

Removes the link and its datasources. Requires link:manage. Cloud storage under the link's prefix is cleaned up afterwards, and the link can be brought back with POST /links/{url_token}/restore while it is still inside its undo window.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Restore a deleted link

Reverses a delete while the link is still inside its undo window: the link comes back with the bytes it still holds, reappears in the owner's list and is announced again as link.created. Bytes already purged are not restored. Requires link:manage, the same permission as the delete it undoes.

A caller who may not manage the link, an unknown token, a link that was never deleted and one whose bytes are already gone all answer 404 link_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Report a link for abuse

Files an abuse report on a link from anyone holding its token; no session is required. A signed-in reporter is identified by their account, an anonymous one by their IP.

Attachments are evidence the reporter chose to show from content already decrypted on their side. link_key is the reporter's explicit permission to open the link's content for this report and only ever arrives in that field: the fragment of url is stripped before storage.

A repeat report by the same reporter on the same link accumulates into the existing one and answers its id.

Authorizations:
NoneSessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
url
required
string [ 1 .. 2048 ] characters

The share URL the reporter used. Its fragment, which may carry the link key, is stripped before storage.

description
string <= 4000 characters

The reporter's own words about the abuse.

contact_email
string <= 320 characters

Email for follow-up. Deleted together with the report once it is handled.

link_key
string <= 512 characters

The link's share key, attached deliberately as permission to open the content for this report. Useful only on a link with a cloud datasource.

Array of objects <= 4 items

Evidence pieces, at most 4, at most 8 MiB decoded in total.

Responses

Request samples

Content type
application/json
{
  • "description": "string",
  • "contact_email": "string",
  • "link_key": "string",
  • "attachments": [
    ]
}

Response samples

Content type
application/json
{
  • "report_id": "0198a9e2-5b1a-7c3e-9f00-2a4b6c8d0e1f"
}

List link ACL entries

Returns every ACL entry on the link: who holds which permissions. Entries carry no key material — the caller's own wrapped share key comes from GET /links/{url_token}, and nobody else's is ever returned. Requires link:read.

The ETag response header carries the link's ACL version; pass it as If-Match on linksAclGrant and linksAclRevoke.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Grant link access

Grants permissions on a link to a subject: public, authenticated, one user or agent, or a workspace's members. A user subject's optional encrypted_share_key goes to the vault; workspace member keys travel separately via POST /vault/keys/batch. Requires link:manage.

The first grant answers status: granted, an identical repeat status: unchanged, and a repeat with different scopes 409 already_granted_different — revoke and grant again to overwrite. If-Match with the ACL ETag answers 412 acl_version_mismatch instead of racing a concurrent change.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

header Parameters
If-Match
string

The link's ACL ETag as last seen by the client. When present and stale the call answers 412 acl_version_mismatch; when absent the change is unconditional.

Request Body schema: application/json
required
subject
required
string (SubjectSelector) <= 128 characters

ACL subject selector — one string naming who a grant addresses:

  • any — anyone, including anonymous callers
  • user:* — any authenticated user
  • user:<id> — one user
  • agent:<id> — one agent team:* is rejected; use any for a world grant.
scopes
required
Array of strings (LinkPermission)
Items Enum: "link:read" "link:write" "link:manage"

Access levels to grant. The server stores only the highest level requested.

encrypted_share_key
string <byte> <= 1024 characters

The link's share key wrapped to the recipient's identity public key (base64). Accepted only when subject names one principal. The recipient reads it back from GET /links/{url_token}.

identity_public_key
string <byte> <= 64 characters

The user recipient's identity public key encrypted_share_key was wrapped to (base64). When sent and no longer current, the grant answers 409 identity_key_mismatch.

Responses

Request samples

Content type
application/json
{
  • "subject": "user:usr_a1b2c3d4e5f6g7h8i9j0",
  • "scopes": [
    ],
  • "encrypted_share_key": "string",
  • "identity_public_key": "string"
}

Response samples

Content type
application/json
Example
{
  • "status": "granted"
}

Revoke link access

Removes a subject's ACL entry from the link. Requires link:manage. Revoking a user subject also deletes that recipient's wrapped share key; after revoking a workspace subject, member keys that no longer have access are removed by a later sweep.

If-Match with the link's ACL ETag works as on linksAclGrant, answering 412 acl_version_mismatch when stale.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

header Parameters
If-Match
string

The link's ACL ETag as last seen by the client. When present and stale the call answers 412 acl_version_mismatch; when absent the change is unconditional.

Request Body schema: application/json
required
subject
required
string (SubjectSelector) <= 128 characters

ACL subject selector — one string naming who a grant addresses:

  • any — anyone, including anonymous callers
  • user:* — any authenticated user
  • user:<id> — one user
  • agent:<id> — one agent team:* is rejected; use any for a world grant.

Responses

Request samples

Content type
application/json
{
  • "subject": "user:usr_a1b2c3d4e5f6g7h8i9j0"
}

Response samples

Content type
application/json
{
  • "status": "ok"
}

Start upload transfer session

Opens an upload transfer session. The cloud datasource comes back with a presigned PUT URL per file named in the body.

A link without estimated_bytes gets no cloud datasource, and a cloud-only link answers 409 estimate_required. A declared size over the ceiling answers 403 size_limit_exceeded. The ceiling is estimated_bytes plus an allowance of 10 % or 512 KiB, whichever is larger, capped by the tier storage cap, and bounds the session's per-path sum; re-presigning a path replaces its size.

A session idle for 5 minutes is abandoned; later calls answer 404 transfer_not_found, and linksUploadUpdate keeps it alive.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
optional

Files to presign when the session opens; the cloud datasource then carries a PUT URL per file in presigned_files. Omit the body to mint nothing up front and presign on demand instead.

presigned
boolean
Default: false

Ignored — presigned is the only access form the server issues.

Array of objects (TransferFileRequest)

Files to presign up front: a PUT each on upload, a GET each on download. Optional — omit it and mint URLs on demand with /presign, which takes the same shape, if_match / if_none_match included.

Responses

Request samples

Content type
application/json
{
  • "presigned": false,
  • "files": [
    ]
}

Response samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "datasources": [
    ]
}

Update upload progress

Reports upload progress on an open session and keeps it alive: repeating unchanged stats stops an idle session from being abandoned.

Reported bytes are held against the link's declared estimated_bytes plus an allowance of 10 % or 512 KiB, whichever is larger; over that the call answers 403 size_limit_exceeded and the client must abort the transfer. A link that declared no estimate is not checked.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
transfer_id
required
string

Transfer session ID

object (TransferStats)

Aggregate transfer statistics

Array of objects (TransferDatasourceStat)

Per-datasource transfer statistics

Array of objects (TransferFileStat)

Per-file progress. Only clients using /upload/files and /download/files populate it; a flat client omits it and keeps sending aggregate stats.

object

Additional transfer metadata (client-specific)

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "stats": {
    },
  • "datasources": [
    ],
  • "files": [
    ],
  • "info": { }
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Re-mint presigned upload URLs

Mints fresh presigned PUT URLs for the named files on an open upload session, to renew a URL that expired mid-transfer or to add files after the start. A path outside the link's prefix — absolute, leading-slash, holding .., a backslash or a control byte, or empty — answers invalid_request.

A file may carry if_match or if_none_match; the matching header then joins the signature and comes back in PresignedFile.headers to be sent verbatim.

Issuance is as on linksUploadStart, and re-presigning a path replaces its previous size, so renewing an expired URL is free.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
required
Array of objects (TransferFileRequest)

Responses

Request samples

Content type
application/json
{
  • "files": [
    ]
}

Response samples

Content type
application/json
{}

Begin a multipart upload

Starts a multipart upload for one large file and returns its upload_id. Mint part URLs with /multipart/parts, then finish with /multipart/complete or /multipart/abort. The path is validated under the link's prefix, else invalid_request.

Issuance is as on linksUploadPresign: a link without estimated_bytes answers 409 estimate_required, and a declared size over the ceiling answers 403 size_limit_exceeded. The size joins the session's per-path sum under the same ceiling, and beginning the same path again replaces its size.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
path
required
string

File path relative to the link prefix; a path outside it is invalid_request.

size
integer <int64> >= 0

Declared total stored size, held to the same ceiling as a single-shot presign (403 size_limit_exceeded). /multipart/complete re-checks the parts and may answer 409 storage_quota_exceeded.

Responses

Request samples

Content type
application/json
{
  • "path": "large/video.mp4",
  • "size": 5368709120
}

Response samples

Content type
application/json
{
  • "path": "large/video.mp4",
  • "upload_id": "2~aBc123EXAMPLEuploadId"
}

Mint presigned UploadPart URLs

Mints presigned UploadPart URLs for the named part numbers of an open multipart upload. Callable repeatedly: for the first batch, to add parts when the count was not known up front, or to renew a part URL that expired. An unknown upload_id answers multipart_upload_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
path
required
string
upload_id
required
string

The upload id from /multipart/begin.

part_numbers
required
Array of integers

1-based part indices to presign (1..10000).

Responses

Request samples

Content type
application/json
{
  • "path": "large/video.mp4",
  • "upload_id": "2~aBc123EXAMPLEuploadId",
  • "part_numbers": [
    ]
}

Response samples

Content type
application/json

List uploaded parts of a multipart upload

Lists the parts already stored for one open multipart upload, so a resuming client can skip them and complete with their ETags. An unknown upload_id answers multipart_upload_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
path
required
string
upload_id
required
string

The upload id from /multipart/begin.

Responses

Request samples

Content type
application/json
{
  • "path": "large/video.mp4",
  • "upload_id": "2~aBc123EXAMPLEuploadId"
}

Response samples

Content type
application/json
{
  • "parts": [
    ]
}

List in-progress multipart uploads

Lists the multipart uploads in progress under the link's prefix, for a client that lost its upload_id: pick an entry, then call /multipart/list-parts to see what it already holds. Unlike list-parts, which covers one upload, this covers the whole link.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Responses

Response samples

Content type
application/json
{
  • "uploads": [
    ]
}

Complete a multipart upload

Completes a multipart upload from the (part_number, etag) list the client collected while putting each part. An upload_id that is unknown, already completed or aborted answers multipart_upload_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
path
required
string

File path relative to the link prefix (the one the parts were uploaded under).

upload_id
required
string

The upload id from /multipart/begin.

required
Array of objects (MultipartCompletedPart)

Responses

Request samples

Content type
application/json
{
  • "path": "docs/report.pdf",
  • "upload_id": "2~aBc123EXAMPLEuploadId",
  • "parts": [
    ]
}

Response samples

Content type
application/json
{
  • "etag": "\"d41d8cd98f00b204e9800998ecf8427e-3\""
}

Abort a multipart upload

Aborts a multipart upload and drops its staged parts. An upload_id that is unknown, already completed or aborted answers multipart_upload_not_found, as on the other multipart calls.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
path
required
string

File path relative to the link prefix.

upload_id
required
string

The upload id from /multipart/begin.

Responses

Request samples

Content type
application/json
{
  • "path": "docs/report.pdf",
  • "upload_id": "2~aBc123EXAMPLEuploadId"
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Delete an object under the link prefix

Deletes one object under the link's prefix. Presigned access carries no client-signed delete, so removal goes through this call. Idempotent: deleting a missing key is a no-op. The path is validated under the link's prefix, else invalid_request.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
path
required
string

File path relative to the link prefix.

Responses

Request samples

Content type
application/json
{
  • "path": "docs/report.pdf"
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

List objects under the link prefix

Lists the objects stored under the link's prefix, one page per call. The listing is flat and recursive — every object under prefix is returned and the client groups them into directories itself. Follow next_cursor to page to the end, or pass start_after to begin from a known key. Each object carries its ETag and last-modified time.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
prefix
string

Path prefix to list, relative to the link prefix. Omit or leave empty to list the whole link.

cursor
string

Opaque continuation token from a prior response's next_cursor; omit for the first page.

start_after
string

Return only objects whose key, relative to the link prefix, sorts after this one (exclusive). Applies to the first page only; later pages follow cursor.

Responses

Request samples

Content type
application/json
{
  • "prefix": "docs/",
  • "cursor": "string",
  • "start_after": "wal/wal-000000001234"
}

Response samples

Content type
application/json
{
  • "objects": [
    ],
  • "next_cursor": "string"
}

Open a file-transfer in an upload session

Opens a file-transfer inside an open upload session, giving the link owner per-file visibility. A client that does not need it can use /upload/start, /upload/update and /upload/end alone.

The caller must own the parent session; another actor's session answers 404 rather than 403. An Idempotency-Key header makes a retry safe: the same key returns the same file_transfer_id. At most 128 file-transfers may be open in one session.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
transfer_id
required
string

Parent transfer session ID returned by POST /{direction}/start.

encrypted_metadata
required
string <byte> <= 8192 characters

Opaque base64 blob encrypted with the link's share key; the server never reads inside it. Typically {path, estimated_bytes, ...}. Max 8 KiB after decoding.

estimated_bytes
integer <int64>

Plaintext size hint. When present, /{direction}/files/{id}/end refuses stats.bytes past it plus the overage allowance with 403 size_limit_exceeded.

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "encrypted_metadata": "eyJwYXRoIjoicGhvdG9zL2ltZ18wMDEuanBnIn0=",
  • "estimated_bytes": 0
}

Response samples

Content type
application/json
{
  • "file_transfer_id": "trf_9K2pR4sEXAMPLE1234567890abcdef01"
}

Close a file-transfer in an upload session

Closes a file-transfer. Idempotent: a second call for the same file_transfer_id answers 204 and publishes nothing. The caller must own the parent session; a file-transfer that does not exist, belongs to another session or to another actor answers 404.

When the file was opened with estimated_bytes, stats.bytes over that plus an allowance of 10 % or 512 KiB, whichever is larger, answers 403 size_limit_exceeded; the file stays open and the client must abort it.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

file_transfer_id
required
string

File-transfer ID returned by POST /links/{url_token}/upload/files.

Request Body schema: application/json
required
success
required
boolean

Whether the file transfer completed successfully.

error
string <= 500 characters

Error message if failed. The exact value cancelled marks a user cancellation: the file closes with close_reason: aborted and is not counted as a failure.

object (TransferStats)

Aggregate transfer statistics

Responses

Request samples

Content type
application/json
{
  • "success": true,
  • "error": "string",
  • "stats": {
    }
}

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

End upload transfer session

Ends an upload transfer session. On success: true the stored total in size.actual is reconciled from storage rather than taken from the reported stats, and repeating the call with identical stats changes nothing.

On success: false the client first aborts any multipart upload still in progress. Call this endpoint even when the abort follows a server error. After a size_limit_exceeded the client stops transferring, aborts its multipart uploads and then ends the session with success: false.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
transfer_id
required
string

Transfer session ID

success
required
boolean

Whether transfer completed successfully

error
string <= 500 characters

Error message if failed

object (TransferStats)

Aggregate transfer statistics

Array of objects (TransferDatasourceStat)

Per-datasource transfer statistics

object

Additional transfer metadata (client-specific)

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "success": true,
  • "error": "string",
  • "stats": {
    },
  • "datasources": [
    ],
  • "info": { }
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Abort upload transfer session

Cancels an upload session in progress. It differs from /upload/end with success: false in intent: abort records a deliberate cancellation, with the supplied reason, as link.transfer_aborted. That frame reaches the link owner and the aborting agent only, never the link topic.

Any file-transfers still open are closed, each with its own link.transfer_completed, and the storage the session reserved is released.

The caller may abort only their own open session: another actor's session, an unknown transfer id and a repeat abort all answer 404 transfer_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
transfer_id
required
string

Transfer session ID returned by POST /{direction}/start.

reason
string <= 200 characters

Free-form reason, carried in the link.transfer_aborted event. Optional — a system abort omits it.

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "reason": "user cancelled from UI"
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Start download transfer session

Opens a download transfer session and returns its transfer id and datasources; the cloud datasource carries a presigned GET URL per file named in the body, and linksDownloadPresign mints more or renews an expired one. Requires link:read.

A session idle for 5 minutes is abandoned (link.transfer_completed with close_reason=abandoned) and later calls answer 404 transfer_not_found. A client that holds a session open while only browsing keeps it alive with linksDownloadUpdate.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
optional

Files to presign when the session opens; the cloud datasource then carries a GET URL per file in presigned_files. Omit the body to mint nothing up front and presign on demand instead.

presigned
boolean
Default: false

Ignored — presigned is the only access form the server issues.

Array of objects (TransferFileRequest)

Files to presign up front: a PUT each on upload, a GET each on download. Optional — omit it and mint URLs on demand with /presign, which takes the same shape, if_match / if_none_match included.

Responses

Request samples

Content type
application/json
{
  • "presigned": false,
  • "files": [
    ]
}

Response samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "datasources": [
    ]
}

Update download progress

Reports download progress on an open session and keeps it alive: a client browsing a share without transferring repeats unchanged stats so the session is not abandoned.

Reported bytes are not held against the link's estimated_bytes, which is the uploader's declaration and may lag what the datasources now hold.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
transfer_id
required
string

Transfer session ID

object (TransferStats)

Aggregate transfer statistics

Array of objects (TransferDatasourceStat)

Per-datasource transfer statistics

Array of objects (TransferFileStat)

Per-file progress. Only clients using /upload/files and /download/files populate it; a flat client omits it and keeps sending aggregate stats.

object

Additional transfer metadata (client-specific)

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "stats": {
    },
  • "datasources": [
    ],
  • "files": [
    ],
  • "info": { }
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Re-mint presigned download URLs

Mints fresh presigned GET URLs for the named files on an open download session, to renew a URL that expired mid-transfer. Paths are validated under the link's prefix; an absolute path, a leading slash, a .. segment, a backslash, a control byte or an empty path answers invalid_request, as does if_match or if_none_match, which a download presign does not accept.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
required
Array of objects (TransferFileRequest)

Responses

Request samples

Content type
application/json
{
  • "files": [
    ]
}

Response samples

Content type
application/json
{}

Open a file-transfer in a download session

Opens a file-transfer inside an open download session, giving the link owner per-file visibility. The first file-transfer of a session is the download itself: it moves the link's download_count, and it is where the download cap of the owner's tier or the owner's download_limit refuses with 409 link_download_limit_exceeded — a session that only browses is never counted or refused.

The caller must own the parent session; another actor's session answers 404 rather than 403. An Idempotency-Key header makes a retry safe: the same key returns the same file_transfer_id. At most 128 file-transfers may be open in one session.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
transfer_id
required
string

Parent transfer session ID returned by POST /{direction}/start.

encrypted_metadata
required
string <byte> <= 8192 characters

Opaque base64 blob encrypted with the link's share key; the server never reads inside it. Typically {path, estimated_bytes, ...}. Max 8 KiB after decoding.

estimated_bytes
integer <int64>

Plaintext size hint. When present, /{direction}/files/{id}/end refuses stats.bytes past it plus the overage allowance with 403 size_limit_exceeded.

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "encrypted_metadata": "eyJwYXRoIjoicGhvdG9zL2ltZ18wMDEuanBnIn0=",
  • "estimated_bytes": 0
}

Response samples

Content type
application/json
{
  • "file_transfer_id": "trf_9K2pR4sEXAMPLE1234567890abcdef01"
}

Close a file-transfer in a download session

Closes a file-transfer. Idempotent: a second call for the same file_transfer_id answers 204 and publishes nothing. The caller must own the parent session; a file-transfer that does not exist, belongs to another session or to another actor answers 404.

Reported bytes are not held against the file's estimated_bytes, which is metadata and may lag the file's real size.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

file_transfer_id
required
string

File-transfer ID returned by POST /links/{url_token}/download/files.

Request Body schema: application/json
required
success
required
boolean

Whether the file transfer completed successfully.

error
string <= 500 characters

Error message if failed. The exact value cancelled marks a user cancellation: the file closes with close_reason: aborted and is not counted as a failure.

object (TransferStats)

Aggregate transfer statistics

Responses

Request samples

Content type
application/json
{
  • "success": true,
  • "error": "string",
  • "stats": {
    }
}

Response samples

Content type
application/json
{
  • "code": "not_found",
  • "status": "workspace_not_found",
  • "error": "name is required",
  • "details": {
    }
}

End download transfer session

Ends a download transfer session.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
transfer_id
required
string

Transfer session ID

success
required
boolean

Whether transfer completed successfully

error
string <= 500 characters

Error message if failed

object (TransferStats)

Aggregate transfer statistics

Array of objects (TransferDatasourceStat)

Per-datasource transfer statistics

object

Additional transfer metadata (client-specific)

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "success": true,
  • "error": "string",
  • "stats": {
    },
  • "datasources": [
    ],
  • "info": { }
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Abort download transfer session

Cancels a download session in progress, the call behind a UI cancel button and the mirror of /upload/abort. Any caller holding link:read may abort their own download session; a downloader need not own the link, and a download reserves no storage.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Request Body schema: application/json
required
transfer_id
required
string

Transfer session ID returned by POST /{direction}/start.

reason
string <= 200 characters

Free-form reason, carried in the link.transfer_aborted event. Optional — a system abort omits it.

Responses

Request samples

Content type
application/json
{
  • "transfer_id": "tr_4Fz4sL4iEXAMPLE",
  • "reason": "user cancelled from UI"
}

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

List active transfer peers

Returns the agents currently taking part in a transfer session on this link, uploading or downloading, so a client can show who is transferring without waiting for link.transfer_started frames. The list is empty when no transfer is open, and it is capped at 100 peers. A caller who cannot reach the link gets 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
url_token
required
string

Link URL token

Responses

Response samples

Content type
application/json
{
  • "peers": [
    ]
}

Shares

Share offers between nearby devices. An offer tells another agent that content is available; the recipient accepts or rejects it.

Send share offer to nearby device

Offers a link to another online agent and returns the offer_token that tracks it. An unknown target answers party_not_found, an offline one party_offline.

The recipient receives a share.offered frame and answers with POST /agents/shares/answers; the sender then receives share.answered. On acceptance the recipient starts the download and the sender the upload; on rejection the sender deletes the link.

The offer carries no plaintext description: the recipient unwraps encrypted_share_key and decrypts the link's encrypted_metadata with it to show what is being shared.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
target_agent_id
required
string (AgentID) [ 8 .. 256 ] characters ^agt_[A-Za-z0-9_-]+$

Unique agent identifier, prefixed with agt_. Returned by POST /auth/agents as agent_id and carried in the access token's claims, so no separate header is needed.

url_token
required
string [ 1 .. 256 ] characters

URL token of the link to share, created with POST /links. The recipient downloads the shared content through it.

encrypted_share_key
string <byte> <= 1024 characters

Share key wrapped to the recipient's device_public_key (base64, hybrid encryption). Optional; the sender takes that key from the nearby.updated frame.

Responses

Request samples

Content type
application/json
{
  • "target_agent_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "url_token": "k7mstq3w",
  • "encrypted_share_key": "string"
}

Response samples

Content type
application/json
{
  • "offer_token": "nae0kePa-random-zoo2Fahf"
}

Accept or reject a share offer

Records the recipient's decision on a share offer and pushes a share.answered frame to the sender. The offer_token and the url_token come from the share.offered frame the recipient received; an offer that is not the caller's own, or does not exist, answers offer_record_not_found.

After accept the recipient starts the download with POST /links/{url_token}/download/start and the sender starts the upload with POST /links/{url_token}/upload/start.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
offer_token
required
string [ 1 .. 256 ] characters

Token from the share.offered frame, naming the offer being answered.

answer
required
string
Enum: "accept" "reject"

Decision on the share offer

Responses

Request samples

Content type
application/json
{
  • "offer_token": "nae0kepa1234zoo2fahf5678abcd3q7w",
  • "answer": "accept"
}

Response samples

Content type
application/json
{
  • "status": "ok"
}

Withdraw a pending share offer

Withdraws an offer the caller sent and the recipient has not answered yet. The offer_token is the one returned by POST /agents/shares/offers. If the recipient is online and still has the prompt open, it receives a share.cancelled frame and dismisses it.

Cancelling an unknown, already-answered or someone else's offer is a no-op and still answers 200. The sender remains responsible for deleting the link with DELETE /links/{url_token}.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
offer_token
required
string [ 1 .. 256 ] characters

Token of the offer to withdraw, from POST /agents/shares/offers.

Responses

Request samples

Content type
application/json
{
  • "offer_token": "nae0kepa1234zoo2fahf5678abcd3q7w"
}

Response samples

Content type
application/json
{
  • "status": "ok"
}

RTC

Peer-to-peer transfer between agents over WebRTC:

  1. Register the agent with POST /auth/agents.
  2. Create a link with p2p_storage: true.
  3. Start a session with POST /links/{url_token}/upload/start or download/start.
  4. Call the peer with POST /agents/call; the peer receives call.incoming.
  5. Transfer over the WebRTC data channel, reporting progress with upload/update or download/update.
  6. Close the session with upload/end or download/end.

Initiate WebRTC call to another agent

Opens a WebRTC signaling call to another agent and rings it. The caller passes the callee's agent_id and the transfer_id of its own active transfer session, which is what authorizes the call; the callee must be online.

The response carries the call_id, the STUN/TURN servers to put into the RTCPeerConnection (TURN credentials are time-limited and specific to this call), the relay URLs and the caller's relay jwt. The callee receives a call.incoming frame with the same ICE servers and its own relay jwt. Both sides then exchange SDP and ICE over the relay servers.

Authorizations:
SessionCookieAuthAccessTokenAuth
Request Body schema: application/json
required
callee_id
required
string (AgentID) [ 8 .. 256 ] characters ^agt_[A-Za-z0-9_-]+$

Unique agent identifier, prefixed with agt_. Returned by POST /auth/agents as agent_id and carried in the access token's claims, so no separate header is needed.

transfer_id
required
string [ 32 .. 256 ] characters

Active transfer session authorizing this call. A downloader gets it from POST /links/{url_token}/download/start.

Responses

Request samples

Content type
application/json
{
  • "callee_id": "agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  • "transfer_id": "tr_2p7mc0k2c2tj6f3q2v56y7wz9b1k4n6h"
}

Response samples

Content type
application/json
{
  • "call_id": "eis2heez-random-towiez1i",
  • "ice_servers": [
    ],
  • "relays": [
    ],
  • "jwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImtleS0xIn0.eyJpc3MiOiJzaGFyZS5uaW5qYSIsInN1YiI6ImNhbGxfMTIzIiwiYXVkIjpbInJlbGF5LXNlbmRlciJdLCJzaWQiOiJhZ3RfY2FsbGVyIiwicmlkIjoiYWd0X2NhbGxlZSIsImV4cCI6MTcwMDAwMDAwMH0.signature"
}

Notifications

In-app inbox of the user: workspace invites, vault join requests, link share requests and system alerts. Cursor-paginated, with mark-read and delete. A new entry is also pushed as a notification.created frame.

List in-app notifications

Returns the signed-in user's own inbox, newest first, paginated by cursor. Deleted notifications are never returned. Pass unread_only=true to keep only the unread ones.

unread_count counts every unread notification of the user, not just this page, so a badge needs no second call.

Authorizations:
SessionCookieAuthAccessTokenAuth
query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

offset
integer >= 0
Example: offset=950

Absolute number of items to skip from the start of the list, for page-number UIs: offset = (page - 1) * limit, page count from page_info.total_count. Mutually exclusive with cursor (sending both is validation_failed). Capped by a server-side ceiling (default 10000); deeper requests are validation_failed — narrow with filters instead. Prefer cursor for sequential paging: it is cheaper and stable against concurrent inserts.

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

unread_only
boolean
Default: false

Return only unread notifications.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    },
  • "unread_count": 3
}

Delete a notification

Removes one of the caller's own notifications from the inbox; later listings no longer return it. A notification that belongs to someone else answers the same 404 as a missing one.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
notification_id
required
string

Public ID of the notification.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Mark a notification as read

Marks one of the caller's own notifications read and answers 204. Calling it again changes nothing and keeps the original read_at. A notification that belongs to someone else answers the same 404 as a missing one.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
notification_id
required
string

Public ID of the notification.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Mark all notifications as read

Marks every unread notification of the caller read and returns how many were affected. Answers 429 when called too often.

Authorizations:
SessionCookieAuthAccessTokenAuth

Responses

Response samples

Content type
application/json
{
  • "affected_count": 5
}

Audit

Audit log of the caller's workspaces, filterable by workspace_id.

List audit events for the caller

Returns the audit events visible to the authenticated user: events where the user is the actor or the subject, and events from workspaces the user is a member of. Results are sorted by created_at descending and paged with cursor (sequential) or offset (page-number UIs).

date_from and date_to are inclusive. A span wider than 90 days answers 400 invalid_date_range; with neither bound the feed starts 30 days back. Filtering by a workspace the caller is not a member of answers 403.

Authorizations:
SessionCookieAuthAccessTokenAuth
query Parameters
workspace_id
string
Example: workspace_id=ws_a1b2c3d4e5f6g7h8

Public ID of a workspace to restrict results to. The caller must be a member.

cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

offset
integer >= 0
Example: offset=950

Absolute number of items to skip from the start of the list, for page-number UIs: offset = (page - 1) * limit, page count from page_info.total_count. Mutually exclusive with cursor (sending both is validation_failed). Capped by a server-side ceiling (default 10000); deeper requests are validation_failed — narrow with filters instead. Prefer cursor for sequential paging: it is cheaper and stable against concurrent inserts.

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

action
string
Example: action=link.created

Exact action to match, such as link.created or auth.user_logged_in.

action_prefix
string <= 64 characters ^[a-z_]+(\.[a-z_]+)*\.$
Example: action_prefix=link.

Only actions starting with this prefix. The prefix ends on a segment boundary: link. or vault. for a whole domain, link.transfer_ is not accepted.

session_id
string
Example: session_id=tr_abc123def456xyz7

Restrict results to one transfer session. Not served yet: any value answers 400 invalid_request.

result
string
Enum: "success" "failure" "denied"

Filter by event outcome.

date_from
string <date-time>
Example: date_from=2026-01-01T00:00:00Z

Inclusive lower bound on created_at, RFC3339.

date_to
string <date-time>
Example: date_to=2026-12-31T23:59:59Z

Inclusive upper bound on created_at, RFC3339.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

List audit events for an app

Returns the audit events of one app. Requires the app:manage scope on it; a caller without access to the app is answered 404. Pass current as app_id to read the app of the current context. Paged with cursor from page_info.next_cursor, or with offset for page-number UIs.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
app_id
required
string
Example: current

App public ID, or current for the app of the current context.

query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

offset
integer >= 0
Example: offset=950

Absolute number of items to skip from the start of the list, for page-number UIs: offset = (page - 1) * limit, page count from page_info.total_count. Mutually exclusive with cursor (sending both is validation_failed). Capped by a server-side ceiling (default 10000); deeper requests are validation_failed — narrow with filters instead. Prefer cursor for sequential paging: it is cheaper and stable against concurrent inserts.

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

List audit events for the current app

Returns the audit events of the app of the current context. Requires the app:manage scope. Paged with cursor (sequential) or offset (page-number UIs).

Authorizations:
SessionCookieAuthAccessTokenAuth
query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

offset
integer >= 0
Example: offset=950

Absolute number of items to skip from the start of the list, for page-number UIs: offset = (page - 1) * limit, page count from page_info.total_count. Mutually exclusive with cursor (sending both is validation_failed). Capped by a server-side ceiling (default 10000); deeper requests are validation_failed — narrow with filters instead. Prefer cursor for sequential paging: it is cheaper and stable against concurrent inserts.

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

List audit events for a workspace

Returns the audit events of one workspace: settings changes, members added and removed, invites created and revoked, and permission changes. Requires the workspace:write scope on that workspace. Paged with cursor (sequential) or offset (page-number UIs).

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string
Example: ws_a1b2c3d4e5f6g7h8

Workspace public ID.

query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

offset
integer >= 0
Example: offset=950

Absolute number of items to skip from the start of the list, for page-number UIs: offset = (page - 1) * limit, page count from page_info.total_count. Mutually exclusive with cursor (sending both is validation_failed). Capped by a server-side ceiling (default 10000); deeper requests are validation_failed — narrow with filters instead. Prefer cursor for sequential paging: it is cheaper and stable against concurrent inserts.

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

API Keys

Personal access tokens for programmatic access.

List API keys for the authenticated user

Lists the caller's live (not revoked) API keys, oldest first, cursor-paged. The secret is never shown again — prefix is how a key is told apart; last_used_at is approximate.

Authorizations:
SessionCookieAuthAccessTokenAuth
query Parameters
cursor
string
Example: cursor=g2wAAAABBmN1cnNvcg

Cursor returned by a previous response to continue listing results

offset
integer >= 0
Example: offset=950

Absolute number of items to skip from the start of the list, for page-number UIs: offset = (page - 1) * limit, page count from page_info.total_count. Mutually exclusive with cursor (sending both is validation_failed). Capped by a server-side ceiling (default 10000); deeper requests are validation_failed — narrow with filters instead. Prefer cursor for sequential paging: it is cheaper and stable against concurrent inserts.

limit
integer [ 1 .. 100 ]
Default: 50
Example: limit=50

Maximum number of items to return for this request (default 50)

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "page_info": {
    }
}

Create a new API key

Creates an API key for programmatic access. The token is returned once, here, and never again; prefix is the visible head listings show. Send it as Authorization: Bearer <token>. A key acts as the user with no device session behind it, so sessions and factors are out of its reach.

scopes narrows the key to a subset of the user's content permissions, named as the application's area:level scopes; an unknown name is invalid_scopes, and omitted or empty leaves the key the user's full rights. expires_at in the past is invalid_expires_at; past it the key stops authenticating.

Authorizations:
SessionCookieAuthAccessTokenAuth
header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Request Body schema: application/json
required
name
required
string [ 1 .. 255 ] characters

Label shown in the key listing.

scopes
Array of strings

Permissions to limit the key to. Omitted or empty gives it the user's full rights.

expires_at
string or null <date-time>

When the key stops authenticating. Null for a key that does not expire.

Responses

Request samples

Content type
application/json
{
  • "name": "Automation bot",
  • "scopes": [
    ],
  • "expires_at": "2025-12-01T00:00:00Z"
}

Response samples

Content type
application/json
{
  • "key_id": "key_018f7b2e-5e6f-7c3d-9a8b-4c7e1f3a6d9c",
  • "name": "Automation bot",
  • "prefix": "c6n9rQ2e",
  • "scopes": [
    ],
  • "created_at": "2025-05-20T09:10:00Z",
  • "last_used_at": null,
  • "token": "c6n9rQ2e1kA7p0s3Zt8vWq4Lm7Xy1Bn0",
  • "expires_at": "2025-12-01T00:00:00Z"
}

Revoke an API key

Revokes one of the caller's own keys: it stops authenticating at once (401 from then on) and leaves the listing. Irreversible. A key that is not the caller's, or does not exist, is api_key_not_found.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
key_id
required
string
Example: key_018f7b2e-3c4d-7a1b-9e6f-2a5c8d1e4b7a

Identifier of the API key to revoke

header Parameters
Idempotency-Key
string <= 255 characters
Example: 1d24c9e0-3f1b-4a75-b0d9-6d3b2d9c1a55

Optional key that makes a mutating request retry-safe: a repeat carrying the same key replays the original outcome instead of creating a second one, for as long as the original operation is on record.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

WebSocket

Server-to-client event stream. Browsers connect with the one-time ticket from POST /auth/agents (GET /ws/connect?ticket=…; 30 s TTL, single use, bound to the agent), native clients with a Bearer token (GET /ws/action). Frames are WSMessage variants; subscriptions are managed over /events/subscriptions.

Connect to the event stream with a ticket

Upgrades to the server-to-client WebSocket using the one-time ticket from POST /auth/agents (30 s TTL, bound to the agent). The server streams WSMessage frames; the client sends nothing. Subscriptions are managed over /events/subscriptions and survive reconnects; the caller's own agent:<agent_id> inbox is always delivered.

Pass last_seen_id, the id of the last frame processed, to receive the frames missed while offline before the live stream continues; past the retention window re-read the resource over REST. A subscription.revoked frame means the caller lost that topic.

query Parameters
ticket
required
string
Example: ticket=dGhpcyBpcyBhIHRlc3QgdGlja2V0IGZvciBkZW1v

One-time ticket from POST /auth/agents.

last_seen_id
string
Example: last_seen_id=evt_0192b1e0-7c5a-7f4a-9c1e-3f1e2d4c5b6a

The id of the last frame processed; the frames after it are replayed before the live stream.

Responses

Response samples

Content type
application/json
Example
{
  • "type": "subscription.revoked",
  • "id": "evt_019ead06-e190-7e8a-8ff5-1d75f0b36e58",
  • "topic": "link:k7mstq3w",
  • "timestamp": "2025-01-15T10:30:00.123Z"
}

Connect to the event stream with a Bearer token

Upgrades to the same server-to-client stream as GET /ws/connect, authenticated by Authorization: Bearer <access token> instead of a ticket. For native and CLI clients, which can set headers on the upgrade request; browsers use the ticket flow. last_seen_id works the same way.

Authorizations:
AccessTokenAuthSessionCookieAuth
query Parameters
last_seen_id
string
Example: last_seen_id=evt_0192b1e0-7c5a-7f4a-9c1e-3f1e2d4c5b6a

The id of the last frame processed; the frames after it are replayed before the live stream.

Responses

Response samples

Content type
application/json
{
  • "code": "bad_request",
  • "status": "invalid_request",
  • "error": null
}

Billing

Subscription billing of a Personal workspace: plans, checkout, the current subscription and its entitlements. Payment-provider identifiers never appear in the contract.

  1. Read plans with GET /billing/plans.
  2. Start a checkout with POST /workspaces/{workspace_id}/billing/checkout and an Idempotency-Key header; open the returned checkout_url when present.
  3. Read the subscription and effective entitlements with GET /workspaces/{workspace_id}/billing.
  4. Change plan, update the payment method, cancel at period end or reactivate through the subscription operations; each returns a durable operation to poll.

List published Personal plans

Returns the published Personal offer versions, each with its price and the entitlements it grants. Open to anyone; no session is needed. A deployment that serves no billing catalog answers an empty list, and billing switched off answers 503.

Responses

Response samples

Content type
application/json
{
  • "plans": [
    ]
}

Get Personal workspace billing

Returns the entitlements in force for the workspace and, when one exists, its subscription. Only the workspace owner may call it; a workspace the caller does not own answers 404 rather than 403.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

Responses

Response samples

Content type
application/json
{
  • "workspace_id": "ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53",
  • "entitlements": {
    },
  • "subscription": {
    }
}

Start Personal subscription checkout

Creates a checkout operation for one published offer version and returns it. The workspace owner calls it. ui_mode picks the payment form: hosted, the default, answers with a checkout_url to send the buyer to, embedded with a client_secret and publishable_key. Redirect URLs are chosen by the server, never taken from the request.

The same Idempotency-Key with the same body replays the original operation. 409 answers a reused key with a different body, a workspace that already has an active subscription, a payment still settling, or an unresolved earlier operation.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

header Parameters
Idempotency-Key
required
string [ 1 .. 255 ] characters .*\S.*

Key that makes this billing mutation retry-safe; the same key with the same body replays the original operation

Request Body schema: application/json
required
offer_version_id
required
string [ 1 .. 255 ] characters

Published offer version to subscribe to

ui_mode
string
Enum: "hosted" "embedded"

How the payment form is shown; absent means hosted. An offer with no embedded form answers 400 validation_failed.

Responses

Request samples

Content type
application/json
{
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1",
  • "ui_mode": "hosted"
}

Response samples

Content type
application/json
{
  • "id": "bop_0198a6d7-9b46-7d5c-a501-d75e8b276c63",
  • "kind": "checkout",
  • "state": "succeeded",
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1",
  • "checkout_url": "http://example.com",
  • "client_secret": "string",
  • "publishable_key": "string",
  • "error": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Get a billing operation

Returns one checkout, plan-change, cancellation or reactivation operation of the workspace, with its current state. An operation that belongs to another workspace answers 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

operation_id
required
string^bop_[A-Za-z0-9_-]+$
Example: bop_0198a6d7-9b46-7d5c-a501-d75e8b276c63

Billing operation public ID

Responses

Response samples

Content type
application/json
{
  • "id": "bop_0198a6d7-9b46-7d5c-a501-d75e8b276c63",
  • "kind": "checkout",
  • "state": "succeeded",
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1",
  • "checkout_url": "http://example.com",
  • "client_secret": "string",
  • "publishable_key": "string",
  • "error": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Change a Personal subscription plan

Moves the active subscription to another published offer version at once, with a prorated charge or credit, and returns the operation. The workspace owner calls it. The same Idempotency-Key with the same body replays the original operation; the same key with a different body answers 409, as does an earlier operation that is still unresolved.

A lower quota takes effect once the plan change is confirmed; a higher quota takes effect only after the prorated charge succeeds.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

header Parameters
Idempotency-Key
required
string [ 1 .. 255 ] characters .*\S.*

Key that makes this billing mutation retry-safe; the same key with the same body replays the original operation

Request Body schema: application/json
required
offer_version_id
required
string [ 1 .. 255 ] characters

Published offer version to switch to

Responses

Request samples

Content type
application/json
{
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1"
}

Response samples

Content type
application/json
{
  • "id": "bop_0198a6d7-9b46-7d5c-a501-d75e8b276c63",
  • "kind": "checkout",
  • "state": "succeeded",
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1",
  • "checkout_url": "http://example.com",
  • "client_secret": "string",
  • "publishable_key": "string",
  • "error": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Cancel a subscription at period end

Schedules the active subscription to end after its current paid period and returns the operation; the subscription's cancel_at_period_end turns true. There is no immediate cancellation. The same Idempotency-Key with the same body replays the original operation.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

header Parameters
Idempotency-Key
required
string [ 1 .. 255 ] characters .*\S.*

Key that makes this billing mutation retry-safe; the same key with the same body replays the original operation

Responses

Response samples

Content type
application/json
{
  • "id": "bop_0198a6d7-9b46-7d5c-a501-d75e8b276c63",
  • "kind": "checkout",
  • "state": "succeeded",
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1",
  • "checkout_url": "http://example.com",
  • "client_secret": "string",
  • "publishable_key": "string",
  • "error": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Reactivate a subscription pending cancellation

Removes a scheduled period-end cancellation while the subscription is still running, and returns the operation. The same Idempotency-Key with the same body replays the original operation. A subscription that has no cancellation scheduled answers 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

header Parameters
Idempotency-Key
required
string [ 1 .. 255 ] characters .*\S.*

Key that makes this billing mutation retry-safe; the same key with the same body replays the original operation

Responses

Response samples

Content type
application/json
{
  • "id": "bop_0198a6d7-9b46-7d5c-a501-d75e8b276c63",
  • "kind": "checkout",
  • "state": "succeeded",
  • "offer_version_id": "ofv_sharon-personal-pro-monthly-v1",
  • "checkout_url": "http://example.com",
  • "client_secret": "string",
  • "publishable_key": "string",
  • "error": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Open payment-method management

Returns a short-lived, already-authenticated URL where the workspace owner updates the payment method of the active subscription. The caller needs a signed-in user session. The URL is handed out once and is neither stored nor logged. A workspace with no live subscription answers 404.

Authorizations:
SessionCookieAuthAccessTokenAuth
path Parameters
workspace_id
required
string^ws_[A-Za-z0-9_-]+$
Example: ws_6f16c25a-84d9-4ccf-bc6e-5333c6456f53

Personal workspace public ID

Responses

Response samples

Content type
application/json

Maintenance

Service health check.

Check service health

Reports service liveness and the running version. Public, no authentication. The check is liveness only: it does not probe the database or any downstream dependency, so a 200 means the process is serving, not that every subsystem is healthy.

Responses

Response samples

Content type
application/json
{
  • "status": "ok",
  • "version": "1.0.0"
}