Skip to content

Endpoints

This page documents all available REST API endpoints grouped by domain. For general concepts (authentication, response format, status codes), see the API Reference overview.

Health & Status

GET /api/health

Returns server health status.

Response — 200:

json
{
  "data": {
    "status": "ok"
  }
}

GET /api/settings/auth-status

Returns whether authentication is enabled and OIDC status.

Response — 200:

json
{
  "data": {
    "authEnabled": true,
    "oidcEnabled": false,
    "hasUsers": true
  }
}

Authentication

POST /api/auth/setup

Create the first user (only works when no users exist). This user becomes a super_admin.

Request body:

FieldTypeRequiredDescription
emailstringYesUser email
passwordstringYesUser password
displayNamestringNoDisplay name

Example:

json
{
  "email": "admin@example.com",
  "password": "securepassword",
  "displayName": "Admin"
}

Response — 201:

json
{
  "data": {
    "token": "eyJhbG...",
    "user": {
      "id": "user_001",
      "email": "admin@example.com",
      "displayName": "Admin",
      "role": "super_admin"
    }
  }
}

POST /api/auth/login

Authenticate with email and password.

Request body:

FieldTypeRequiredDescription
emailstringYesUser email
passwordstringYesUser password

Response — 200:

json
{
  "data": {
    "token": "eyJhbG...",
    "user": {
      "id": "user_001",
      "email": "admin@example.com",
      "displayName": "Admin",
      "role": "super_admin"
    }
  }
}

POST /api/auth/register

Register a new user.

Request body:

FieldTypeRequiredDescription
emailstringYesUser email
passwordstringYesUser password
displayNamestringNoDisplay name

Response — 201:

json
{
  "data": {
    "token": "eyJhbG...",
    "user": {
      "id": "user_002",
      "email": "user@example.com",
      "displayName": "User",
      "role": "user"
    }
  }
}

GET /api/auth/me

Get the current authenticated user.

Response — 200:

json
{
  "data": {
    "id": "user_001",
    "email": "admin@example.com",
    "displayName": "Admin",
    "role": "super_admin"
  }
}

Admin Users

WARNING

All admin user endpoints require super_admin or admin role.

GET /api/admin/users

List all users with pagination and search.

Query parameters:

ParameterTypeDescription
searchstringFilter users by name or email
offsetnumberNumber of users to skip (default: 0)
limitnumberMaximum users to return (default: 50, max: 100)

Response — 200:

json
{
  "data": {
    "users": [
      {
        "id": "user_001",
        "email": "admin@example.com",
        "name": "Admin",
        "role": "super_admin",
        "authProvider": "local",
        "avatarUrl": null,
        "isSuperAdmin": true,
        "createdAt": "2025-01-01T00:00:00Z"
      }
    ],
    "total": 1
  }
}

POST /api/admin/users

Create a new user with a generated password.

Request body:

FieldTypeRequiredDescription
emailstringYesUser email
namestringYesDisplay name
rolestringYesRole: super_admin, admin, or user

Response — 201:

json
{
  "data": {
    "user": {
      "id": "user_003",
      "email": "newuser@example.com",
      "name": "New User",
      "role": "user",
      "authProvider": "local",
      "avatarUrl": null,
      "isSuperAdmin": false,
      "createdAt": "2025-06-01T12:00:00Z"
    },
    "generatedPassword": "xK9#mP2vL5nQ"
  }
}

TIP

The generated password is only returned once. Share it securely with the user — they will be prompted to change it on first login.

PATCH /api/admin/users/{id}

Update a user's name or role.

Request body:

FieldTypeRequiredDescription
namestringNoNew display name
rolestringNoNew role (super_admin, admin, or user)

Response — 200:

json
{
  "data": {
    "id": "user_003",
    "email": "newuser@example.com",
    "name": "Updated Name",
    "role": "admin",
    "authProvider": "local",
    "avatarUrl": null,
    "isSuperAdmin": false,
    "createdAt": "2025-06-01T12:00:00Z"
  }
}

DELETE /api/admin/users/{id}

Delete a user. Cannot delete yourself or the last super admin. Owned projects are reassigned to a super admin.

Response — 204 (no body)

POST /api/admin/users/{id}/reset-password

Reset a user's password (local auth only, not available for SSO users).

Response — 200:

json
{
  "data": {
    "generatedPassword": "hN7$kR4wB8mJ"
  }
}

GET /api/admin/users/{id}/check-delete

Check the impact of deleting a user (number of owned projects that would be reassigned).

Response — 200:

json
{
  "data": {
    "ownedProjectsCount": 3
  }
}

Organizations

POST /api/organizations

Create a new organization.

Request body:

FieldTypeRequiredDescription
namestringYesOrganization name

Response — 201:

json
{
  "data": {
    "id": "org_001",
    "name": "My Team"
  }
}

GET /api/organizations

List all organizations the current user belongs to.

Response — 200:

json
{
  "data": [
    {
      "id": "org_001",
      "name": "My Team"
    }
  ]
}

GET /api/organizations/{id}

Get a single organization.

ParameterLocationTypeDescription
idpathstringOrganization ID

Response — 200:

json
{
  "data": {
    "id": "org_001",
    "name": "My Team"
  }
}

PATCH /api/organizations/{id}

Update an organization.

Request body:

FieldTypeRequiredDescription
namestringNoNew organization name

Response — 200:

json
{
  "data": {
    "id": "org_001",
    "name": "Renamed Team"
  }
}

DELETE /api/organizations/{id}

Delete an organization. Fails if the organization still has projects.

Response — 204 (no body)

GET /api/organizations/{id}/members

List organization members.

Response — 200:

json
{
  "data": [
    {
      "userId": "user_001",
      "email": "admin@example.com",
      "displayName": "Admin",
      "role": "admin"
    }
  ]
}

POST /api/organizations/{id}/members

Add a member to an organization.

Request body:

FieldTypeRequiredDescription
emailstringYesUser email to add
rolestringNoMember role (admin or member, defaults to member)

PATCH /api/organizations/{id}/members/{userId}

Update a member's role.

Request body:

FieldTypeRequiredDescription
rolestringYesNew role (admin or member)

DELETE /api/organizations/{id}/members/{userId}

Remove a member from the organization.

Response — 204 (no body)

PUT /api/organizations/{orgId}/default-credential

Set the default credential for an organization.

Request body:

FieldTypeRequiredDescription
credentialIdstringYesCredential ID to set as default

Projects

POST /api/projects

Create a new project.

Request body:

FieldTypeRequiredDescription
namestringYesProject name
pathstringYesAbsolute path to the project directory
organizationIdstringNoOrganization to associate with

Response — 201:

json
{
  "data": {
    "id": "proj_001",
    "name": "My App",
    "path": "/home/user/my-app"
  }
}

GET /api/projects

List all projects accessible to the current user.

Query parameters:

ParameterTypeDescription
organizationIdstringFilter by organization

GET /api/projects/{id}

Get a single project.

DELETE /api/projects/{id}

Delete a project and all its boards, columns, and cards.

Response — 204 (no body)

PATCH /api/projects/reorder

Reorder projects.

Request body:

FieldTypeRequiredDescription
idsstring[]YesOrdered list of project IDs

PATCH /api/projects/{id}/last-opened

Mark a project as last opened (used for restoring UI state).

Response — 204 (no body)

POST /api/projects/{id}/mark-all-read

Mark all cards in a project as read.

Response — 204 (no body)

GET /api/projects/{id}/export

Export the full project as a JSON file.

Response — 200: JSON file download

GET /api/projects/{projectId}/files

List git-tracked files in the project directory.

Response — 200:

json
{
  "data": [
    "src/main.rs",
    "Cargo.toml",
    "README.md"
  ]
}

PUT /api/projects/{projectId}/credential

Set the credential used for this project.

Request body:

FieldTypeRequiredDescription
credentialIdstringYesCredential ID

Project Members

GET /api/projects/{id}/members

List project members.

POST /api/projects/{id}/members

Add a member to a project (by email, creates a pending invitation if user doesn't exist).

Request body:

FieldTypeRequiredDescription
emailstringYesUser email
rolestringNoMember role

PATCH /api/projects/{projectId}/members/{userId}

Update a project member's role.

DELETE /api/projects/{projectId}/members/{userId}

Remove a member from a project.

Response — 204 (no body)

DELETE /api/projects/{projectId}/invitations/{email}

Remove a pending invitation from a project.

Response — 204 (no body)


Boards

GET /api/projects/{projectId}/boards

List all boards in a project.

Response — 200:

json
{
  "data": [
    {
      "id": "board_001",
      "name": "Main Board",
      "projectId": "proj_001",
      "position": 0
    }
  ]
}

POST /api/projects/{projectId}/boards

Create a new board in a project.

Request body:

FieldTypeRequiredDescription
namestringYesBoard name
templateIdstringNoBoard template to use for initial columns

Response — 201:

json
{
  "data": {
    "id": "board_002",
    "name": "Sprint Board",
    "projectId": "proj_001",
    "position": 1
  }
}

PATCH /api/projects/{projectId}/boards/{boardId}

Update a board.

Request body:

FieldTypeRequiredDescription
namestringNoNew board name

DELETE /api/projects/{projectId}/boards/{boardId}

Delete a board and its columns/cards.

Response — 204 (no body)

PATCH /api/projects/{projectId}/boards/reorder

Reorder boards within a project.

Request body:

FieldTypeRequiredDescription
idsstring[]YesOrdered list of board IDs

Columns

GET /api/projects/{projectId}/columns

List columns in a project.

Query parameters:

ParameterTypeDescription
boardIdstringFilter columns by board

Response — 200:

json
{
  "data": [
    {
      "id": "col_001",
      "name": "Backlog",
      "columnType": "backlog",
      "position": 0,
      "boardId": "board_001"
    }
  ]
}

POST /api/projects/{projectId}/columns

Create a new column.

Request body:

FieldTypeRequiredDescription
namestringYesColumn name
columnTypestringYesType: backlog, active, done, archive
boardIdstringYesBoard to add the column to
promptstringNoAgent prompt for this column

PATCH /api/projects/{projectId}/columns/{columnId}

Update a column.

Request body:

FieldTypeRequiredDescription
namestringNoColumn name
promptstringNoAgent prompt

DELETE /api/projects/{projectId}/columns/{columnId}

Delete a column.

Response — 204 (no body)

PATCH /api/projects/{projectId}/columns/reorder

Reorder columns within a board.

Request body:

FieldTypeRequiredDescription
idsstring[]YesOrdered list of column IDs

POST /api/projects/{projectId}/columns/{columnId}/clean-all

Delete all cards in a column.

Response — 204 (no body)


Cards (Project-Scoped)

GET /api/projects/{projectId}/cards

List all cards in a project.

Response — 200:

json
{
  "data": [
    {
      "id": "card_001",
      "title": "Fix login bug",
      "columnId": "col_001",
      "position": 0,
      "hasActiveAgent": false
    }
  ]
}

GET /api/projects/{projectId}/cards/{cardId}

Get a single card with full details.

PATCH /api/projects/{projectId}/cards/{cardId}

Update a card.

Request body:

FieldTypeRequiredDescription
titlestringNoCard title
promptstringNoCard prompt
labelsstring[]NoCard labels

DELETE /api/projects/{projectId}/cards/{cardId}

Delete a card.

Query parameters:

ParameterTypeDescription
forcebooleanSkip worktree check (true to force delete)

Response — 204 (no body)

POST /api/projects/{projectId}/cards/{cardId}/view

Mark a card as viewed by the current user.

Response — 204 (no body)

PATCH /api/projects/{projectId}/cards/{cardId}/move

Move a card to a different column.

Request body:

FieldTypeRequiredDescription
columnIdstringYesTarget column ID
positionnumberNoPosition in the target column

GET /api/projects/{projectId}/cards/{cardId}/diff

Get the git diff for a card's worktree.

Response — 200:

json
{
  "data": {
    "diff": "diff --git a/src/main.rs ...",
    "stats": {
      "filesChanged": 3,
      "insertions": 42,
      "deletions": 10
    }
  }
}

PATCH /api/projects/{projectId}/cards/{cardId}/auto-mode

Toggle auto mode for a card.

Request body:

FieldTypeRequiredDescription
enabledbooleanYesEnable or disable auto mode

POST /api/projects/{projectId}/cards/{cardId}/approve-auto

Approve automatic progression to the next pipeline column.

Response — 204 (no body)

POST /api/projects/{projectId}/cards/{cardId}/complete

Mark a card as complete.

Response — 204 (no body)

POST /api/projects/{projectId}/cards/{cardId}/activate

Activate a deferred card.

Response — 204 (no body)

PUT /api/projects/{projectId}/cards/{cardId}/credential

Set the credential for a specific card.

Request body:

FieldTypeRequiredDescription
credentialIdstringYesCredential ID

Card Pipeline

GET /api/projects/{projectId}/cards/{cardId}/pipeline

Get the pipeline configuration for a card.

Response — 200:

json
{
  "data": {
    "steps": [
      {
        "columnId": "col_001",
        "columnName": "Backlog",
        "prompt": "Analyze the requirements"
      }
    ]
  }
}

PUT /api/projects/{projectId}/cards/{cardId}/pipeline

Update the pipeline configuration for a card.

Request body:

FieldTypeRequiredDescription
stepsobject[]YesPipeline step definitions

Card Attachments

GET /api/projects/{projectId}/cards/{cardId}/attachments

List attachments on a card.

POST /api/projects/{projectId}/cards/{cardId}/attachments

Upload an attachment to a card. Use multipart/form-data.

Form fields:

FieldTypeRequiredDescription
filefileYesThe file to upload

Response — 201:

json
{
  "data": {
    "id": "att_001",
    "filename": "screenshot.png",
    "contentType": "image/png",
    "size": 45320
  }
}

GET /api/attachments/{attachmentId}

Download an attachment file.

Response — 200: File download with appropriate Content-Type header.


Cards (External Flat API)

These endpoints provide a simplified interface for external integrations — no project scoping in the URL.

POST /api/cards

Create a card (includes projectId in the body).

Request body:

FieldTypeRequiredDescription
projectIdstringYesTarget project ID
titlestringYesCard title
promptstringNoCard prompt / description

Response — 201:

json
{
  "data": {
    "id": "card_001",
    "title": "Fix login bug",
    "projectId": "proj_001",
    "columnId": "col_001",
    "position": 0
  }
}

GET /api/cards/{id}

Get card detail with column name and session history.

Response — 200:

json
{
  "data": {
    "id": "card_001",
    "title": "Fix login bug",
    "columnName": "In Progress",
    "sessions": []
  }
}

GET /api/cards/{id}/status

Lightweight card status check.

Response — 200:

json
{
  "data": {
    "id": "card_001",
    "title": "Fix login bug",
    "columnName": "Backlog",
    "hasActiveAgent": false
  }
}

Agents

GET /api/projects/{projectId}/cards/{cardId}/agent

Get the current agent session for a card.

Response — 200:

json
{
  "data": {
    "sessionId": "sess_001",
    "status": "running",
    "cardId": "card_001"
  }
}

POST /api/projects/{projectId}/cards/{cardId}/agent/cancel

Cancel the running agent on a card.

Response — 204 (no body)

GET /api/projects/{projectId}/cards/{cardId}/agent/sessions

List all agent sessions for a card.

GET /api/projects/{projectId}/cards/{cardId}/agent/history

Get session summaries (condensed history).

GET /api/projects/{projectId}/cards/{cardId}/agent/messages

Get agent messages with pagination.

Query parameters:

ParameterTypeDescription
sessionIdstringFilter by session
sincestringMessages after this timestamp
beforestringMessages before this timestamp
limitnumberMaximum messages to return

POST /api/projects/{projectId}/cards/{cardId}/agent/message

Send a message to the agent.

Request body:

FieldTypeRequiredDescription
contentstringYesMessage content

POST /api/projects/{projectId}/cards/{cardId}/agent/retry

Retry the last agent action.

Response — 204 (no body)

POST /api/projects/{projectId}/cards/{cardId}/agent/relaunch

Relaunch the agent (new session).

Response — 204 (no body)

POST /api/projects/{projectId}/cards/{cardId}/agent/relay

Relay context to the agent (context handoff for continuation).

Response — 204 (no body)

POST /api/projects/{projectId}/cards/{cardId}/agent/plan-approve

Approve or revise the agent's plan.

Request body:

FieldTypeRequiredDescription
approvedbooleanYesWhether to approve the plan
feedbackstringNoFeedback or revision instructions

POST /api/projects/{projectId}/cards/{cardId}/summary

Generate a summary of the card's agent work.

Response — 200:

json
{
  "data": {
    "summary": "Implemented login form validation with email format checking..."
  }
}

GET /api/projects/{projectId}/active-agents

List all active agents in a project.

Response — 200:

json
{
  "data": [
    {
      "cardId": "card_001",
      "cardTitle": "Fix login bug",
      "status": "running"
    }
  ]
}

Assistant

GET /api/projects/{projectId}/assistant

Get the assistant status for a project.

POST /api/projects/{projectId}/assistant/message

Send a message to the project assistant.

Request body:

FieldTypeRequiredDescription
contentstringYesMessage content

Credentials

GET /api/credentials

List all credentials.

POST /api/credentials

Create a new credential.

Request body:

FieldTypeRequiredDescription
namestringYesCredential display name
providerstringYesProvider type (e.g., anthropic, openai)
apiKeystringNoAPI key (if not using OAuth)

Response — 201:

json
{
  "data": {
    "id": "cred_001",
    "name": "My Anthropic Key",
    "provider": "anthropic"
  }
}

GET /api/credentials/{id}

Get a single credential (API key is masked).

Response — 200:

json
{
  "data": {
    "id": "cred_001",
    "name": "My Anthropic Key",
    "provider": "anthropic",
    "apiKey": "sk-ant-...****"
  }
}

PATCH /api/credentials/{id}

Update a credential.

Response — 200:

json
{
  "data": {
    "id": "cred_001",
    "name": "Renamed Key",
    "provider": "anthropic"
  }
}

DELETE /api/credentials/{id}

Delete a credential.

Response — 204 (no body)

POST /api/credentials/{id}/verify

Verify that a credential is valid by testing the API connection.

Response — 200:

json
{
  "data": {
    "valid": true
  }
}

GET /api/credentials/{id}/usage

Get usage statistics for a credential.

POST /api/credentials/oauth/init

Start an OAuth flow for credential setup.

Request body:

FieldTypeRequiredDescription
providerstringYesOAuth provider

GET /api/credentials/{id}/oauth/status

Check the OAuth authorization status for a credential.


Pre-Sessions (Prompt Planning)

Pre-sessions let you refine a card prompt before starting the agent.

POST /api/projects/{projectId}/pre-session

Create a new pre-session.

Request body:

FieldTypeRequiredDescription
cardIdstringYesCard to create the pre-session for

POST /api/projects/{projectId}/pre-session/{sessionId}/message

Send a message in the pre-session conversation.

Request body:

FieldTypeRequiredDescription
contentstringYesMessage content

POST /api/projects/{projectId}/pre-session/{sessionId}/accept

Accept the refined prompt from the pre-session.

POST /api/projects/{projectId}/pre-session/{sessionId}/cancel

Cancel the pre-session.

GET /api/projects/{projectId}/pre-session/{sessionId}/messages

Get all messages in a pre-session.


Board Templates

GET /api/board-templates

List all board templates.

Response — 200:

json
{
  "data": [
    {
      "id": "tpl_001",
      "name": "Kanban Default",
      "columns": [
        { "name": "Backlog", "columnType": "backlog" },
        { "name": "In Progress", "columnType": "active" },
        { "name": "Done", "columnType": "done" }
      ]
    }
  ]
}

POST /api/board-templates

Create a board template.

Request body:

FieldTypeRequiredDescription
namestringYesTemplate name
columnsobject[]YesColumn definitions

PATCH /api/board-templates/{id}

Update a board template.

DELETE /api/board-templates/{id}

Delete a board template.

Response — 204 (no body)


Filesystem

GET /api/filesystem/browse

Browse directories on the server.

Query parameters:

ParameterTypeDescription
pathstringDirectory path to browse

GET /api/filesystem/read

Read a file's content from the server.

Query parameters:

ParameterTypeDescription
pathstringFile path to read

Scripts

GET /api/projects/{projectId}/scripts

List scripts configured for a project.

POST /api/projects/{projectId}/scripts

Create a new script.

Request body:

FieldTypeRequiredDescription
namestringYesScript display name
commandstringYesShell command to run

PUT /api/projects/{projectId}/scripts/reorder

Reorder scripts.

Request body:

FieldTypeRequiredDescription
idsstring[]YesOrdered list of script IDs

GET /api/projects/{projectId}/scripts/status

Get current script execution status.

PUT /api/projects/{projectId}/scripts/{scriptId}

Update a script.

DELETE /api/projects/{projectId}/scripts/{scriptId}

Delete a script.

Response — 204 (no body)

POST /api/projects/{projectId}/scripts/{scriptId}/run

Run a script.

POST /api/projects/{projectId}/scripts/{scriptId}/cancel

Cancel a running script.


Slash Commands

GET /api/projects/{projectId}/slash-commands

List available slash commands for a project.

Response — 200:

json
{
  "data": [
    {
      "name": "/test",
      "description": "Run project tests"
    }
  ]
}

Settings & Configuration

GET /api/settings/auth

Get authentication settings.

PUT /api/settings/auth

Enable or disable authentication.

Request body:

FieldTypeRequiredDescription
enabledbooleanYesEnable or disable auth

WARNING

Requires super_admin role.

GET /api/settings/notifications

Get notification preferences.

PUT /api/settings/notifications

Update notification preferences.

GET /api/settings/context-relay

Get context relay mode setting.

PUT /api/settings/context-relay

Update context relay mode.

GET /api/settings/keyboard-shortcuts

Get custom keyboard shortcuts.

PUT /api/settings/keyboard-shortcuts

Update custom keyboard shortcuts.


User Settings

GET /api/user/settings

Get user preferences (theme, sidebar state, etc.).

Response — 200:

json
{
  "data": {
    "theme": "dark",
    "highContrast": false,
    "sidebarExpanded": true,
    "agentReasoning": true,
    "statusLine": true,
    "hiddenToolTypes": []
  }
}

PUT /api/user/settings

Update user preferences.


Search for users by name or email.

Query parameters:

ParameterTypeDescription
qstringSearch query

Response — 200:

json
{
  "data": [
    {
      "id": "user_001",
      "email": "admin@example.com",
      "displayName": "Admin"
    }
  ]
}

Panel States

Panel states persist UI layout preferences per card.

GET /api/projects/{projectId}/panel-states

Get all panel states for a project.

Response — 200:

json
{
  "data": {}
}

PUT /api/projects/{projectId}/panel-states

Update panel states.

Response — 200:

json
{
  "data": {}
}

DELETE /api/projects/{projectId}/panel-states/{cardId}

Delete a panel state for a specific card.

Response — 204 (no body)


Logs

GET /api/logs

Get server log entries.

Query parameters:

ParameterTypeDescription
levelstringFilter by log level (info, warn, error)
searchstringFull-text search in log messages
limitnumberMaximum entries to return

Log Export Configuration

GET /api/settings/log-export

Get log export configuration.

PUT /api/settings/log-export

Update log export configuration (e.g., external log aggregation).

POST /api/settings/log-export/test

Test the log export connection.


Export & Backup

POST /api/export

Export organization data.

POST /api/backups

Create a full backup.

WARNING

Requires super_admin role.

GET /api/backups

List available backups.

WARNING

Requires super_admin role.

GET /api/backups/{filename}

Download a backup file.

WARNING

Requires super_admin role.


OIDC (Single Sign-On)

GET /api/settings/oidc

Get the OIDC configuration.

WARNING

Requires super_admin role.

POST /api/settings/oidc

Set the OIDC configuration.

Request body:

FieldTypeRequiredDescription
issuerUrlstringYesOIDC issuer URL
clientIdstringYesClient ID
clientSecretstringYesClient secret

WARNING

Requires super_admin role.

DELETE /api/settings/oidc

Remove the OIDC configuration.

WARNING

Requires super_admin role.

POST /api/oidc/test

Test the current OIDC configuration.

GET /api/oidc/login

Initiate the OIDC login flow. Redirects the user to the identity provider.

GET /api/oidc/callback

OIDC callback handler. Receives the authorization code and completes authentication.


WebSocket

GET /api/ws

Upgrade to a WebSocket connection for real-time events. All card mutations, agent status changes, and notifications are broadcast to connected clients.

Query parameters:

ParameterTypeDescription
tokenstringBearer token for authentication (required when auth is enabled)

TIP

When authentication is enabled, pass the token as a query parameter since WebSocket connections do not support custom headers during the upgrade handshake. The server closes the connection with code 4001 if the token is missing or invalid.

Connect using a standard WebSocket client:

javascript
// Without auth
const ws = new WebSocket('ws://localhost:37100/api/ws');

// With auth
const ws = new WebSocket('ws://localhost:37100/api/ws?token=eyJhbG...');

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('Event:', message.type, message.data);
};

After connecting, subscribe to a project to receive its events:

javascript
ws.send(JSON.stringify({ type: 'subscribe', payload: { projectId: 1 } }));