Developer Docs

WebSocket API

Persistent bidirectional connection for real-time updates, live events, and request/response messaging.

WebSocket API

The WebSocket API provides a persistent connection for receiving real-time events and sending actions. It is the primary data interface used by the TRCR web application and supports all operations available via REST.

Endpoint: wss://api.trcr.pro/ws

Connection Flow

WebSocket authentication uses a ticket-based system. First obtain a short-lived ticket via REST, then present it when connecting.

Step 1: Get a WebSocket Ticket

POST https://api.trcr.pro/api/v1/auth/ws-ticket
Authorization: Bearer YOUR_ACCESS_TOKEN

Response:
{ "ticket": "wst_a1b2c3d4e5f6..." }

Tickets expire after 30 seconds and can only be used once.

Step 2: Connect and Authenticate

Open a WebSocket connection and immediately send an authentication message:

import WebSocket from "ws";

const ws = new WebSocket("wss://api.trcr.pro/ws");

ws.on("open", () => {
  // Authenticate with the ticket
  ws.send(JSON.stringify({
    action: "authenticate",
    payload: { ticket: "wst_a1b2c3d4e5f6..." },
    request_id: "req_1"
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());

  if (msg.request_id === "req_1" && msg.status === "ok") {
    console.log("Authenticated successfully");
  }

  // Handle real-time events
  if (msg.event) {
    console.log("Event:", msg.event, msg.data);
  }
});

ws.on("close", (code, reason) => {
  console.log("Disconnected:", code, reason.toString());
});

ws.on("error", (err) => {
  console.error("WebSocket error:", err);
});

Message Format

All messages are JSON objects. Client-to-server messages follow this structure:

{
  "action": "string",         // The action to perform
  "payload": { ... },         // Action-specific data
  "request_id": "string"      // Optional: correlate with response
}

Server Responses

For request/response actions, the server replies with:

{
  "status": "ok" | "error",   // Result status
  "data": { ... },            // Response data (on success)
  "error": {                  // Error details (on failure)
    "code": "string",
    "message": "string"
  },
  "request_id": "string"      // Matches your request_id
}

Server Events (Push)

Real-time events are pushed without a request_id:

{
  "event": "string",          // Event type
  "data": { ... }             // Event payload
}

Real-Time Events

After authentication, you automatically receive events for entities you have access to. No subscription is needed.

EntityChanged

Fired whenever any entity is created, updated, or deleted. This is the most common event.

{
  "event": "EntityChanged",
  "data": {
    "entity_type": "task",           // task, project, time_entry, invoice, etc.
    "entity_id": "tsk_abc123",
    "action": "updated",             // created, updated, deleted
    "changes": {                     // Only present on "updated"
      "status": { "from": "open", "to": "done" }
    },
    "entity": { ... },              // Full entity data
    "user_id": "usr_xyz",           // User who made the change
    "timestamp": "2026-03-28T14:30:00Z"
  }
}

ChatMessage

Fired when a new chat message is sent to a channel you belong to.

{
  "event": "ChatMessage",
  "data": {
    "channel_id": "ch_abc123",
    "message": {
      "id": "msg_001",
      "content": "Hello team!",
      "user_id": "usr_xyz",
      "user_name": "Jane Doe",
      "created_at": "2026-03-28T14:30:00Z"
    }
  }
}

NotificationCreated

Fired when a new notification is created for the user.

{
  "event": "NotificationCreated",
  "data": {
    "id": "ntf_001",
    "type": "task_assigned",
    "title": "You were assigned to 'Implement login page'",
    "entity_type": "task",
    "entity_id": "tsk_abc123",
    "created_at": "2026-03-28T14:30:00Z"
  }
}

TimerUpdate

Fired when the current user starts or stops a timer from another device or tab.

{
  "event": "TimerUpdate",
  "data": {
    "running": true,
    "time_entry": {
      "id": "te_abc123",
      "description": "Working on API docs",
      "project_id": "prj_abc",
      "start_time": "2026-03-28T14:00:00Z"
    }
  }
}

UserPresence

Fired when a team member comes online or goes offline.

{
  "event": "UserPresence",
  "data": {
    "user_id": "usr_xyz",
    "status": "online",        // online, away, offline
    "last_seen": "2026-03-28T14:30:00Z"
  }
}

Action Reference

The following actions can be sent over WebSocket. They mirror the REST API endpoints.

Authentication

ActionPayloadDescription
authenticate{ ticket }Authenticate the connection

Tasks

ActionPayloadDescription
tasks.list{ project_id?, status?, page?, per_page? }List tasks
tasks.get{ id }Get a single task
tasks.create{ title, project_id, start_date?, due_date?, ... }Create a task (start_date and due_date are optional YYYY-MM-DD; start_date must be on or before due_date)
tasks.create_bulk{ tasks: [{ title, project_id, start_date?, due_date?, ... }] }Create multiple tasks in one call (each item accepts start_date)
tasks.update{ id, start_date?, due_date?, ...fields }Update a task (start_date accepted; must be on or before due_date)
tasks.update_status{ id, status }Update task status; triggers email and in-app notifications for reporter and assignees
tasks.delete{ id }Delete a task
tasks.reorder{ task_id, position, group_id? }Reorder a task within its group
tasks.add_comment{ task_id, body }Add a comment
tasks.delete_comment{ task_id, comment_id }Delete a comment
tasks.checklist.list{ task_id }List checklist items for a task
tasks.checklist.create{ task_id, title }Add a checklist item (requires manage_tasks)
tasks.checklist.toggle{ item_id }Toggle checklist item completed state (any org member)
tasks.checklist.delete{ item_id }Delete a checklist item (requires manage_tasks)
tasks.dependency.list{ task_id }List task dependencies (blocked_by / blocks)
tasks.dependency.add{ task_id, depends_on_id, dependency_type }Add a dependency link; dependency_type is blocks or blocked_by (requires manage_tasks)
tasks.dependency.remove{ task_id, dependency_id }Remove a dependency link (requires manage_tasks)
tasks.dependencies.list_all{ org_id }List every task dependency in the organization in one call (returns only edges where both endpoint tasks are visible to the caller)

Projects

ActionPayloadDescription
projects.list{ status?, search? }List projects
projects.get{ id }Get a project
projects.create{ name, ... }Create a project
projects.update{ id, ...fields }Update a project
projects.delete{ id }Delete a project

Task Groups

ActionPayloadDescription
task_groups.list{ project_id }List task groups for a project
task_groups.create{ project_id, name }Create a task group
task_groups.update{ project_id, group_id, name }Update a task group
task_groups.delete{ project_id, group_id }Delete a task group
task_groups.reorder{ project_id, order }Reorder task groups (order is array of group IDs)
task_groups.toggle_collapsed{ project_id, group_id }Toggle collapsed state for the current user

Task Statuses

ActionPayloadDescription
task_statuses.list{}List custom task statuses
task_statuses.create{ code, label, color, ... }Create a custom status
task_statuses.update{ id, ...fields }Update a custom status
task_statuses.delete{ id }Delete a custom status

Task Recurrences

ActionPayloadDescription
task_recurrences.create{ task_id, recurrence_pattern }Set a task to repeat (daily, weekly, monthly, or cron)
task_recurrences.get{ recurrence_id }Get a recurrence rule
task_recurrences.update{ recurrence_id, recurrence_pattern }Update a recurrence rule
task_recurrences.delete{ recurrence_id }Stop a recurrence

Organizations

ActionPayloadDescription
organizations.list{}List user's organizations
organizations.get{ id }Get an organization
organizations.create{ name, slug? }Create an organization
organizations.update{ id, ...fields }Update an organization
organizations.delete{ id }Delete an organization

Members

ActionPayloadDescription
members.list{ role? }List organization members
members.invite{ email, role? }Invite a member
members.update{ user_id, role }Update a member's role
members.remove{ user_id }Remove a member

Time Entries

ActionPayloadDescription
time_entries.start{ org_id, project_id?, task_id?, description? }Start a timer
time_entries.stop{ org_id }Stop the running timer
time_entries.current{ org_id }Get the currently running timer (null if none)
time_entries.get{ org_id, entry_id }Get a single time entry by ID
time_entries.list{ org_id, from?, to?, project_id?, task_id?, user_id?, billable? }List time entries with optional filters
time_entries.create{ org_id, started_at, stopped_at, project_id?, task_id?, description? }Create a manual time entry
time_entries.update{ org_id, entry_id, ...fields }Update a time entry
time_entries.delete{ org_id, entry_id }Delete a time entry

Invoices

ActionPayloadDescription
invoices.list{ client_id?, status? }List invoices
invoices.get{ id }Get an invoice
invoices.create{ client_id, due_date, ... }Create an invoice
invoices.update{ id, ...fields }Update an invoice
invoices.delete{ id }Delete an invoice
invoices.generate{ id, from?, to? }Auto-generate line items from time entries
invoices.send{ id, message? }Send to client
invoices.mark_paid{ id, payment_method? }Mark as paid

Clients

ActionPayloadDescription
clients.list{ search? }List clients
clients.get{ id }Get a client
clients.create{ name, ... }Create a client
clients.update{ id, ...fields }Update a client
clients.delete{ id }Delete a client

Chat

ActionPayloadDescription
chat.channels.list{}List channels
chat.messages.list{ channel_id, before?, limit? }List messages
chat.messages.send{ channel_id, content, reply_to? }Send a message
chat.messages.delete{ channel_id, message_id }Delete a message
chat.reactions.add{ message_id, emoji }Add a reaction
chat.reactions.remove{ message_id, emoji }Remove a reaction
chat.typing{ channel_id }Send typing indicator

Search

ActionPayloadDescription
search{ q, type?, limit? }Search across entities

Notifications

ActionPayloadDescription
notifications.list{ unread? }List notifications
notifications.mark_read{ ids? }Mark as read

Reports

ActionPayloadDescription
reports.timesheet{ from, to, user_id?, project_id?, client_id?, description?, invoice_id?, group_by? }Generate timesheet report. invoice_id is an optional UUID that restricts the report to time entries linked to this invoice.
reports.revenue{ from, to, client_id?, group_by? }Generate revenue report
reports.utilization{ from, to, user_id?, invoice_id?, target_hours? }Generate utilization report. invoice_id is an optional UUID that restricts the report to time entries linked to this invoice.
reports.profitability{ from, to, group_by? }Generate profitability report

Labels

ActionPayloadDescription
labels.list{}List all labels
labels.create{ name, color }Create a label
labels.update{ id, name?, color? }Update a label
labels.delete{ id }Delete a label

Milestones

ActionPayloadDescription
milestones.list{ org_id, project_id }List a Space's milestones, ordered by target date (requires access to the Space)
milestones.create{ org_id, project_id, title, target_date, color?, description? }Create a milestone (target_date is YYYY-MM-DD; color defaults to #6366f1; requires manage_tasks)
milestones.update{ org_id, milestone_id, title?, target_date?, color?, description? }Update a milestone; send description: null to clear it, omit a field to leave it unchanged (requires manage_tasks)
milestones.delete{ org_id, milestone_id }Delete a milestone (requires manage_tasks)

Sidebar

ActionPayloadDescription
sidebar.get_collapsed{}Get collapsed sidebar node IDs
sidebar.toggle_collapsed{ project_id }Toggle a sidebar node collapsed state

Dashboard

ActionPayloadDescription
dashboard.summary{}Get dashboard summary (total tasks, done, overdue)

Audit

ActionPayloadDescription
audit.list{ user_id?, action?, entity_type?, from?, to? }List audit log entries (admin only)

Heartbeat / Keep-Alive

The server sends a ping frame every 30 seconds. If your WebSocket library does not automatically respond with a pong, you must handle it manually. If no pong is received within 10 seconds, the server closes the connection.

You can also send a ping action to test the connection:

{ "action": "ping" }

// Response:
{ "action": "pong", "timestamp": "2026-03-28T14:30:00Z" }

Reconnection

Connections may be dropped due to network issues, server deployments, or inactivity. Implement automatic reconnection with exponential backoff:

function connect(attempt = 0) {
  const ws = new WebSocket("wss://api.trcr.pro/ws");

  ws.on("open", () => {
    attempt = 0;
    // Re-authenticate with a fresh ticket
    authenticateWs(ws);
  });

  ws.on("close", () => {
    const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
    console.log(`Reconnecting in ${delay}ms...`);
    setTimeout(() => connect(attempt + 1), delay);
  });

  ws.on("error", () => {
    ws.close();
  });

  return ws;
}

Rate Limits

WebSocket connections are limited to 60 messages per minute. If you exceed this limit, the server sends an error message and may close the connection.

{
  "status": "error",
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many messages. Please slow down."
  }
}

Connection Limits

  • Maximum 5 concurrent WebSocket connections per user
  • Maximum message size: 64 KB
  • Idle connections (no messages for 5 minutes) are closed
  • Authentication must complete within 10 seconds of connecting