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:
Node.js Python
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 Action Payload Description authenticate { ticket } Authenticate the connection
Tasks Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description search { q, type?, limit? } Search across entities
Notifications Action Payload Description notifications.list { unread? } List notifications notifications.mark_read { ids? } Mark as read
Reports Action Payload Description 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 Action Payload Description 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 Action Payload Description 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 Action Payload Description sidebar.get_collapsed {} Get collapsed sidebar node IDs sidebar.toggle_collapsed { project_id } Toggle a sidebar node collapsed state
Dashboard Action Payload Description dashboard.summary {} Get dashboard summary (total tasks, done, overdue)
Audit Action Payload Description 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:
Node.js Python
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