Documentation

Inbox and communications

Unified chat + email threads, status workflow, and the AI assistant auto-response loop.

The inbox is a single view onto every conversation an org has — chat from the website widget, email replies, SMS through the Phone module — surfaced through one set of REST endpoints under /crm/inbox/* and /crm/communications/*. Every message references a contact (or anonymous visitor), every thread carries a status, and every reply can be authored by a staff user, a customer, or an AI assistant.

Conversations

GET/crm/inbox/conversationsJWT
GET/crm/inbox/conversations/:partiesJWT

conversations lists the threads visible to the current user. :parties is a +-joined pair of party identifiers (e.g. cust-abc+staff-xyz) and returns the thread between exactly those two parties — useful for direct-message UIs.

Each conversation carries:

{
  id: string;
  channel: 'chat' | 'email' | 'sms' | 'whatsapp';
  parties: string[];           // contact SK + staff SK
  contactId?: string;
  subject?: string;
  lastMessage: { id: string; text: string; from: string; createdAt: string };
  unreadCount: number;
  status: 'open' | 'pending' | 'closed' | 'snoozed';
  assignedTo?: string;
  tags?: string[];
  priority?: 'low' | 'normal' | 'high';
  updatedAt: string;
}

The channel discriminator drives the rendering — same shape, different message body conventions.

Messages

GET/crm/inbox/messagesJWT
GET/crm/inbox/messages/:idJWT
POST/crm/chat-message/createJWT
POST/crm/chat-message/updateJWT
DELETE/crm/chat-message/delete/:idJWT

/crm/inbox/messages returns messages within a conversation (filter by conversationId, paginate with before/after). The chat-message/* endpoints write — they share storage with the chat module, so a chat sent through the widget appears in the inbox without extra wiring.

// Send a staff reply
await fetch('/api/crm/chat-message/create', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({
    conversationId: convoId,
    text: 'Thanks Alice — taking a look now.',
    attachments: [],
    isInternalNote: false,
  }),
});

Set isInternalNote: true and the message is stored on the conversation but not delivered to the customer — used for handoff comments between staff.

Status workflow

POST/crm/inbox/update-status/:messageId/:statusJWT
POST/crm/inbox/updateJWT
DELETE/crm/inbox/delete/:idJWT

:status accepts open | pending | closed | snoozed. Snooze takes an optional ?until=<iso>; the conversation auto-reopens at that time.

The looser inbox/update endpoint accepts a body to change assignedTo, tags, priority, subject on a conversation:

await fetch('/api/crm/inbox/update', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({
    conversationId: convoId,
    assignedTo: 'staff-xyz',
    priority: 'high',
    tags: ['billing', 'refund-request'],
  }),
});

Notifications and push tokens

GET/crm/inbox/notifications/:id?JWT
POST/crm/inbox/save-push-tokenJWT

notifications/:id? returns the staff user's pending in-app notifications. save-push-token registers a mobile device token (FCM / APNs) for push delivery — the inbox app uses this for new-message pushes.

Templates

POST/crm/send-templateJWT

Send a saved message template (subject + body with merge tags) to a contact through the conversation's channel. Templates are stored in the broadcast module and managed via the admin.

await fetch('/api/crm/send-template', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({
    templateName: 'order-shipped',
    contactId: 'cust-abc',
    channel: 'email',
    variables: { orderNumber: 'ORD-2026-0042', tracking: '1Z9...784' },
  }),
});

Voice and SMS history

The Communications controller is the read-side aggregator over phone and SMS activity:

GET/crm/communicationsJWT
GET/crm/communications/smsJWT
GET/crm/communications/callsJWT
GET/crm/communications/recordingsJWT
GET/crm/communications/recordings/:recordingSidJWT
GET/crm/communications/statsJWT

recordings/:recordingSid returns the carrier-hosted audio URL plus a transcript if one's been generated. To trigger transcription:

POST/crm/communications/recordings/transcribeJWT

Twilio onboarding

POST/crm/communications/twilio/setupJWT
GET/crm/communications/twilio/verifyJWT
GET/crm/communications/twilio/available-numbersJWT
GET/crm/communications/twilio/system-phoneJWT

The setup endpoint connects an existing Twilio sub-account to the org; verify confirms the credentials work; available-numbers searches Twilio for purchasable numbers in a given area code; system-phone returns the org's primary outbound number. Number purchase and SMS routing then move to the Phone module.

AI assistant auto-responses

The CRM module ships an ai-assistant sub-controller that wires the inbox to the AI module. When an incoming message arrives on a conversation marked aiAssist: true, the assistant generates a draft reply and either:

  • Suggests — saves the draft as a suggested_reply on the conversation; staff sees it in the UI and clicks Send.
  • Auto-sends — if the conversation rules permit (low complexity score, customer not VIP-flagged, response is high-confidence), sends without staff review.

Configuration lives at /crm/ai-assistant/* — set the prompt, the channel allow-list, and the auto-send confidence threshold. The assistant uses the same streaming agent surface as the rest of the AI module; auto-replies appear in the conversation as messages with from: 'ai-assistant'.

The AI assistant module is documented separately under the AI section. See the AI overview when it's published; until then, treat the assistant as a configurable hook that consumes conversation messages and produces drafts.

Realtime delivery

The chat / inbox endpoints are REST-only for read and write. Live delivery (typing indicators, "new message" toasts, presence) goes through the WebSocket gateway documented in the Chat module — the inbox UI subscribes to that gateway and updates its REST-fetched view. Both layers reference the same message records, so polling-only clients stay correct, just less snappy.

Events fired

  • inbox.message_created
  • inbox.message_received — fired on incoming (customer→staff) only
  • inbox.conversation_assigned
  • inbox.conversation_status_changed
  • inbox.template_sent

Wire these into Automation for Slack alerts, SLA timers, or escalation flows.