Business Processes
Step-by-step guides for implementing common workflows using the TRCR API.
Business Processes
This guide walks through the most common workflows in TRCR, showing the sequence of API calls needed to implement each process end to end.
1. Time Tracking Flow
The time tracking flow starts with a running timer and ends with a billable entry that can be included on an invoice.
Start a Timer
A user starts working on a task. Only one timer can be active per user at a time. The project_id is optional -- timers can run without a project.
// Start the timer
const res = await fetch("https://api.trcr.pro/api/v1/time-entries/start", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
project_id: "prj_abc",
task_id: "tsk_xyz",
description: "Implementing search feature",
billable: true,
}),
});
const entry = await res.json();
// entry.id is the time entry ID, entry.start_time is when it startedStop the Timer
When the user finishes working, stop the timer by its entry ID. The server calculates the duration and finalizes the entry.
// Stop the running timer (entryId from the start response)
const res = await fetch(`${BASE}/time-entries/${entryId}/stop`, {
method: "POST",
headers: { Authorization: "Bearer " + token },
});
const { data: completed } = await res.json();
// completed.duration_secs is the total seconds tracked
console.log(`Tracked ${(completed.duration_secs / 3600).toFixed(2)} hours`);Create a Manual Entry
Alternatively, create a time entry directly without using the timer (e.g., logging time retroactively):
const res = await fetch("https://api.trcr.pro/api/v1/time-entries", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
project_id: "prj_abc",
task_id: "tsk_xyz",
description: "Code review",
started_at: "2026-03-28T14:00:00Z",
stopped_at: "2026-03-28T15:30:00Z",
billable: true,
}),
});Add to Invoice
Once time entries are recorded, generate invoice line items from unbilled entries:
// Generate invoice lines from time entries
await fetch("https://api.trcr.pro/api/v1/invoices/inv_abc123/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
from: "2026-03-01",
to: "2026-03-31",
}),
});2. Project Management
Projects organize tasks, time entries, and team members. Here is the typical flow from creation to ongoing management.
Create a Project
// 1. Create the project
const project = await fetch("https://api.trcr.pro/api/v1/projects", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
name: "Website Redesign",
client_id: "cli_abc",
billable: true,
hourly_rate: 15000, // $150/hr in cents
color: "#6366f1",
}),
}).then(r => r.json());
// 2. Add team members
await fetch(`https://api.trcr.pro/api/v1/projects/${project.id}/members`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
user_id: "usr_designer",
role: "member",
}),
});
// 3. Create initial tasks
const tasks = [
{ title: "Design homepage mockups", priority: "high", assignee_id: "usr_designer" },
{ title: "Implement responsive layout", priority: "medium" },
{ title: "Set up CI/CD pipeline", priority: "low" },
];
for (const task of tasks) {
await fetch("https://api.trcr.pro/api/v1/tasks", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
...task,
project_id: project.id,
}),
});
}Track Progress
Update task statuses as work progresses and monitor overall project health:
// Move task to in-progress
await fetch("https://api.trcr.pro/api/v1/tasks/tsk_abc123", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({ status: "in_progress" }),
});
// Add a checklist to track sub-tasks
await fetch("https://api.trcr.pro/api/v1/tasks/tsk_abc123/checklists", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
title: "Implementation Steps",
items: [
{ text: "Set up project structure", checked: false },
{ text: "Create components", checked: false },
{ text: "Add tests", checked: false },
],
}),
});
// Add a comment with progress update
await fetch("https://api.trcr.pro/api/v1/tasks/tsk_abc123/comments", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
body: "Homepage design approved by client. Moving to implementation.",
}),
});3. Invoicing Flow
The invoicing flow takes tracked time and turns it into a professional invoice that can be sent to clients and tracked through to payment.
// 1. Create an invoice for the client
const invoice = await fetch("https://api.trcr.pro/api/v1/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
client_id: "cli_abc",
project_id: "prj_abc",
due_date: "2026-04-30",
currency: "USD",
tax_rate: 21,
notes: "Payment due within 30 days. Bank details on file.",
}),
}).then(r => r.json());
// 2. Auto-generate line items from unbilled time entries
await fetch(`https://api.trcr.pro/api/v1/invoices/${invoice.id}/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
from: "2026-03-01",
to: "2026-03-31",
}),
});
// 3. Optionally add manual line items
await fetch(`https://api.trcr.pro/api/v1/invoices/${invoice.id}/line-items`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
description: "Project management fee",
quantity: 1,
unit_price: 50000, // $500 flat fee
}),
});
// 4. Send the invoice to the client
await fetch(`https://api.trcr.pro/api/v1/invoices/${invoice.id}/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
message: "Hi, please find attached the invoice for March 2026.",
}),
});
// 5. When payment is received, mark as paid
await fetch(`https://api.trcr.pro/api/v1/invoices/${invoice.id}/mark-paid`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
payment_method: "bank_transfer",
paid_at: "2026-04-15T00:00:00Z",
}),
});
// 6. Record the payment details
await fetch("https://api.trcr.pro/api/v1/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
invoice_id: invoice.id,
amount: invoice.total,
method: "bank_transfer",
notes: "Wire transfer ref #98765",
}),
});4. Team Management
Set up an organization, invite team members, assign roles, and manage project access.
// 1. Create an organization
const org = await fetch("https://api.trcr.pro/api/v1/organizations", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
name: "Acme Corp",
slug: "acme",
}),
}).then(r => r.json());
// 2. Invite team members
const invites = [
{ email: "alice@acme.com", role: "admin" },
{ email: "bob@acme.com", role: "member" },
{ email: "carol@acme.com", role: "member" },
];
for (const invite of invites) {
await fetch("https://api.trcr.pro/api/v1/members/invite", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify(invite),
});
}
// 3. Once members join, assign them to projects
await fetch("https://api.trcr.pro/api/v1/projects/prj_abc/members", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
user_id: "usr_alice",
role: "manager",
}),
});
// 4. Update a member's role if needed
await fetch("https://api.trcr.pro/api/v1/members/mem_bob", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({ role: "admin" }),
});
// 5. Remove a member if they leave
await fetch("https://api.trcr.pro/api/v1/members/mem_carol", {
method: "DELETE",
headers: { Authorization: "Bearer " + token },
});5. Client Management
Manage client relationships from initial setup through ongoing interactions and billing.
// 1. Create a client
const client = await fetch("https://api.trcr.pro/api/v1/clients", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
name: "Acme Corp",
email: "billing@acme.com",
phone: "+1-555-0100",
address: {
street: "123 Main St",
city: "San Francisco",
state: "CA",
zip: "94102",
country: "US",
},
currency: "USD",
hourly_rate: 15000,
}),
}).then(r => r.json());
// 2. Add contacts
await fetch(`https://api.trcr.pro/api/v1/clients/${client.id}/contacts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
name: "John Smith",
email: "john@acme.com",
role: "CTO",
}),
});
await fetch(`https://api.trcr.pro/api/v1/clients/${client.id}/contacts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
name: "Jane Doe",
email: "jane@acme.com",
role: "Project Manager",
}),
});
// 3. Log client activities
await fetch(`https://api.trcr.pro/api/v1/clients/${client.id}/activities`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
type: "meeting",
title: "Kickoff meeting",
description: "Discussed project scope and timeline. Client approved initial budget.",
date: "2026-03-28T10:00:00Z",
}),
});
// 4. Create a project for the client
const project = await fetch("https://api.trcr.pro/api/v1/projects", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
name: "Acme Website",
client_id: client.id,
billable: true,
hourly_rate: client.hourly_rate,
}),
}).then(r => r.json());
// 5. Generate an invoice for the client
const invoice = await fetch("https://api.trcr.pro/api/v1/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
client_id: client.id,
project_id: project.id,
due_date: "2026-04-30",
}),
}).then(r => r.json());6. Real-Time Sync
TRCR uses WebSocket events to keep all connected clients in sync. Every entity change in the system publishes an EntityChanged event that propagates to all authorized connections.
How It Works
- A user performs an action (e.g., updates a task via REST or WebSocket)
- The backend service layer processes the action and persists it to PostgreSQL
- The service layer publishes an
EntityChangedevent to Redis pub/sub - The WebSocket connection manager receives the event from Redis
- The manager fans out the event to all WebSocket connections that have access to the entity
- Each connected client receives the event and updates its local state
Listening for Changes
import WebSocket from "ws";
const ws = new WebSocket("wss://api.trcr.pro/ws");
ws.on("open", () => {
ws.send(JSON.stringify({
action: "authenticate",
payload: { ticket: wsTicket },
}));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.event === "EntityChanged") {
const { entity_type, entity_id, action, entity } = msg.data;
switch (entity_type) {
case "task":
if (action === "created") addTaskToUI(entity);
if (action === "updated") updateTaskInUI(entity_id, entity);
if (action === "deleted") removeTaskFromUI(entity_id);
break;
case "time_entry":
if (action === "updated") refreshTimeEntryList();
break;
case "invoice":
refreshInvoiceList();
break;
}
}
if (msg.event === "TimerUpdate") {
updateTimerUI(msg.data);
}
if (msg.event === "ChatMessage") {
appendMessage(msg.data.channel_id, msg.data.message);
}
if (msg.event === "NotificationCreated") {
showNotification(msg.data);
}
});Cross-Tab Sync
When using TRCR in a browser with multiple tabs, the WebSocket connection in one tab receives events for actions performed in other tabs. This ensures all views stay consistent. The same pattern applies across devices -- changes on a mobile device appear instantly on the desktop and vice versa.
7. Search
TRCR uses Meilisearch for full-text search across all entities. Search indexes are updated automatically when entities are created, updated, or deleted.
How Indexing Works
- When an entity is created or updated, the service layer sends it to Meilisearch
- Meilisearch indexes the entity with typo-tolerance and relevance ranking
- Indexes are scoped by organization -- users can only search within their org
- Deleted entities are removed from the index immediately
Unified Search
The search endpoint queries across all entity types simultaneously and returns results ranked by relevance:
// Search across everything
const res = await fetch("https://api.trcr.pro/api/v1/search?q=website+redesign", {
headers: { Authorization: "Bearer " + token },
});
const { results } = await res.json();
// results is an array of:
// { type: "task", id: "tsk_abc", title: "...", score: 0.95 }
// { type: "project", id: "prj_xyz", title: "...", score: 0.87 }
// Filter by type
const taskRes = await fetch("https://api.trcr.pro/api/v1/search?q=login+bug&type=task&limit=5", {
headers: { Authorization: "Bearer " + token },
});Search Features
- Typo tolerance: "websit redeisgn" still finds "Website Redesign"
- Prefix search: "web" matches "Website", "Webhook", etc.
- Ranking: Results are ordered by relevance with exact matches ranked highest
- Scoping: Results are automatically filtered to the user's organization
8. Notifications
Notifications keep users informed about important events. They are delivered both in-app (via WebSocket and REST) and optionally via email.
Notification Triggers
Notifications are automatically created when:
- A task is assigned to a user
- Someone comments on a task the user is involved in
- A task the user is watching changes status
- The user is mentioned in a chat message
- An invoice is paid
- A team member joins the organization
- A project deadline is approaching (24 hours before)
Delivery Channels
Notifications are delivered through two channels:
- In-app (WebSocket): A
NotificationCreatedevent is pushed to the user's WebSocket connection immediately. - Email: If email notifications are enabled in the user's preferences, an email is sent. Emails are batched -- if multiple notifications occur within 5 minutes, they are grouped into a single email digest.
Managing Notifications
// List unread notifications
const res = await fetch("https://api.trcr.pro/api/v1/notifications?unread=true", {
headers: { Authorization: "Bearer " + token },
});
const { data: notifications, unread_count } = await res.json();
// Mark specific notifications as read
await fetch("https://api.trcr.pro/api/v1/notifications/mark-read", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
ids: notifications.map(n => n.id),
}),
});
// Mark all as read (omit ids)
await fetch("https://api.trcr.pro/api/v1/notifications/mark-read", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({}),
});Notification Preferences
Users can customize which notifications they receive and through which channels:
// Get current preferences
const prefs = await fetch("https://api.trcr.pro/api/v1/notifications/preferences", {
headers: { Authorization: "Bearer " + token },
}).then(r => r.json());
// Update preferences
await fetch("https://api.trcr.pro/api/v1/notifications/preferences", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
email_enabled: true,
task_assigned: true,
task_commented: true,
invoice_paid: true,
chat_mentioned: true,
}),
});Real-Time Notification Handling
// Listen for notifications via WebSocket
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.event === "NotificationCreated") {
const notification = msg.data;
// Show a toast or badge in your UI
showToast({
title: notification.title,
type: notification.type,
onClick: () => navigateTo(notification.entity_type, notification.entity_id),
});
// Update unread count
incrementUnreadBadge();
}
});