Omnichannel Inboxes & Bridges
QuotyAI uses a dual-channel architecture to handle customer conversations across every platform. Whether you want complete data ownership with Native Inboxes, or prefer to leverage your existing Chatwoot setup with Inbox Bridges, QuotyAI gives you the flexibility to choose the right approach for your business.
Two Ways to Handle Conversations
QuotyAI implements two distinct approaches for managing conversational channels:
- Native Inboxes: Channels where conversation history is stored natively within QuotyAIβs MongoDB database β you own the data completely.
- Inbox Bridges: Proxy connections to external Chatwoot instances β QuotyAI processes messages but conversation history lives externally.
Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Channel Layer β
β Telegram β Facebook β WhatsApp β Instagram β Voice/LiveKit β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
ββββββββββββββββ΄βββββββββββββββ
β β
βββββββββΌβββββββββ βββββββββΌβββββββββ
β Native Inboxes β β Inbox Bridges β
β (Data Owner) β β (Proxy Layer) β
βββββββββ¬βββββββββ βββββββββ¬βββββββββ
β β
βΌ βΌ
βββββββββββββββββ ββββββββββββββββββ
β QuotyAI DB β β Chatwoot API β
β (Full Store) β β (Ext. Store) β
βββββββββ¬ββββββββ βββββββββ¬βββββββββ
β β
ββββββββββββββββ¬βββββββββββ
βΌ
ββββββββββββββββββββββββββββββββ
β Unified AI Processing β
β (Sales + Management β
β Assistants) β
ββββββββββββββββββββββββββββββββ
Native Inboxes: Complete Data Ownership
What Are Native Inboxes?
Native inboxes are communication channels where QuotyAI owns the complete conversation lifecycle. Every message, conversation thread, and contact profile is stored directly in your MongoDB database. You get full control, complete audit trails, and rich attachment processing with OCR.
Supported Channels
| Channel | Enum Value | Status |
|---|---|---|
| Telegram | TELEGRAM |
β Active |
| Facebook Messenger | FACEBOOK |
β Active |
INSTAGRAM |
β Active | |
WHATSAPP |
β Active | |
| Voice (LiveKit) | LIVEKIT |
β Active |
Data Storage Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MongoDB (QuotyAI DB) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β native-inboxes β Inbox configs & credentials β
β native-inbox-conversations β Conversation threads β
β native-inbox-contacts β Unified contact profiles β
β native-inbox-telegram-messages β Telegram messages β
β native-inbox-facebook-messages β Facebook messages β
β voice-inboxes β Voice inbox configs β
β voice-inbox-sessions β Voice call sessions β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Core Data Models
NativeInboxDoc (Inbox Configuration)
export interface NativeInboxDoc {
_id: ObjectId;
tenantId: ObjectId;
businessEntityId: ObjectId; // Links to your business
name: string;
channel: NativeInboxChannelType; // TELEGRAM, FACEBOOK, etc.
status: NativeInboxStatus; // 'active' | 'disabled' | 'disconnected'
// Channel-specific credentials (embedded)
credentials: NativeInboxChannelCredentialsDoc;
// Telegram: { botToken, botUsername }
// Facebook: { pageId, accessToken, appSecret }
// WhatsApp: { phoneNumberId, businessAccountId }
webhookUrl: string;
webhookSecret?: string; // For verification
// AI configuration
defaultAiReplyAssistantAssignment?: ConversationAiAssignmentDoc;
createdAt: Date;
updatedAt?: Date;
deletedAt?: Date; // Soft delete
}
NativeInboxConversationDoc (Conversation Thread)
export interface NativeInboxConversationDoc {
_id: ObjectId;
tenantId: ObjectId;
inboxId: ObjectId;
contactId: ObjectId;
channel: NativeInboxChannelType;
// AI assignment (can override inbox-level default)
aiReplyAssistantAssignment?: ConversationAiAssignmentDoc;
customAttributes?: Record<string, unknown>;
externalId: string; // Telegram chat ID, Facebook PSID, etc.
lastInboundMessage?: { text?: string; createdAt: Date };
lastOutboundMessage?: { text?: string; createdAt: Date };
createdAt: Date;
updatedAt?: Date;
deletedAt?: Date;
}
BaseNativeInboxMessageDoc (Message)
export interface BaseNativeInboxMessageDoc {
_id: ObjectId;
tenantId: ObjectId;
conversationId: ObjectId;
inboxId: ObjectId;
direction: MessageDirection; // 'inbound' | 'outbound'
status: MessageStatusEnum; // 'sent' | 'delivered' | 'read' | 'failed'
text?: string;
sender: NativeInboxMessageSenderDoc;
// { id: string; type: 'customer' | 'ai' | 'human_agent' | 'system' }
// Rich attachments (all stored with GCS URLs)
attachedImages?: NativeInboxMessageImageAttachmentDoc[]; // + OCR data
attachedVideos?: NativeInboxMessageVideoAttachmentDoc[];
attachedAudios?: NativeInboxMessageAudioAttachmentDoc[];
attachedFiles?: NativeInboxMessageFileAttachmentDoc[];
attachedLocations?: NativeInboxMessageLocationAttachmentDoc[];
attachedContacts?: NativeInboxMessageContactAttachmentDoc[];
attachedStickers?: NativeInboxMessageStickerAttachmentDoc[];
externalId?: string; // Platform message ID
metadata?: NativeInboxMessageMetadata; // { runId for observability }
visibleForCustomer: boolean;
visibleForAi: boolean;
createdAt: Date;
deletedAt?: Date;
}
How Native Inboxes Work
Message Receiving Flow
Customer sends message
β
Telegram/Facebook API sends webhook to QuotyAI
β
Webhook Receiver (verify secret, check rate limits)
β
Event Queue (priority-based, async processing)
β
Conversation Workflow Service
β
βββ Find/Create Contact (unified profile)
βββ Find/Create Conversation
βββ Store Message (with OCR for images)
βββ Call AI Assistant
β
AI generates response
β
Send via Channel SDK (Telegram SDK, Facebook SDK)
β
Store outbound message in MongoDB
Event Queue System
The UnifiedEventQueueService provides robust asynchronous processing:
export enum QueuedEventType {
MESSAGE = 'message',
EDITED_MESSAGE = 'edited_message',
CALLBACK_QUERY = 'callback_query',
POSTBACK = 'postback',
// ... 20+ event types supported
}
export enum EventPriority {
HIGH = 0, // User messages, payments
NORMAL = 1, // Delivery confirmations
LOW = 2, // Read receipts, reactions
}
Features:
- Priority-based processing (HIGH β NORMAL β LOW)
- Automatic retry with exponential backoff (max 3 retries)
- Rate limiting per chat ID
- Idempotency (prevents duplicate processing)
- Concurrent processing with configurable limits
Key Services
| Service | Responsibility |
|---|---|
TelegramChannelLifecycleService |
Inbox CRUD, bot token validation, webhook setup/delete |
TelegramWebhookReceiverService |
Receive webhooks, verify secret, queue events |
TelegramConversationWorkflowService |
Process messages, store in DB, manage contacts |
FacebookChannelLifecycleService |
Same for Facebook/Instagram |
UnifiedNativeInboxLifecycleService |
Unified read operations across all native inboxes |
UnifiedNativeInboxConversationService |
Conversation search, pagination, AI assignment |
Inbox Bridges: Proxy Architecture
What Are Inbox Bridges?
Inbox bridges are proxy connections to external Chatwoot instances. Unlike native inboxes, QuotyAI does NOT store conversation history. Instead, it proxies requests to the Chatwoot API in real-time while handling AI processing locally.
Key Concept: Proxy vs Storage
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β QuotyAI Stores β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β chatwoot-inbox-bridges β Bridge configs only β
β external-chatwoot-accounts β External account credentials β
β external-chatwoot-reviewed-conversations β Review markersβ
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
β Proxies requests
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Chatwoot Instance β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Conversations (lives here) β
β Messages (lives here) β
β Contacts (lives here) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Supported via Chatwoot
Since Chatwoot supports 10+ channels, inbox bridges indirectly support:
- Facebook Messenger, Instagram, WhatsApp
- Email, Website chat widgets
- Slack, Microsoft Teams
- Any custom channel via API
Data Models
ChatwootInboxBridgeDoc (Bridge Configuration)
export interface ChatwootInboxBridgeDoc {
_id: ObjectId;
tenantId: ObjectId;
name: string;
isActive: boolean;
// Reference to external Chatwoot account
parentExternalAccountId?: ObjectId;
// Chatwoot connection details
chatwootHost?: string; // e.g., "https://chatwoot.example.com"
chatwootUserAdminToken?: string; // Admin API token
chatwootAccountId?: number; // Numeric account ID
chatwootAgentBotId?: number; // Bot ID in Chatwoot
chatwootAgentBotAccessToken?: string; // Bot access token
// Inbox identification
createdChatwootInboxId?: number; // For internal QuotyAI Chatwoot
latestConnectedChatwootInboxId?: number; // Last connected inbox
// AI Assistant connection
apiKeyId: ObjectId; // For webhook authentication
assistantRole?: AssistantRole; // 'sales' | 'accountant'
assistantId?: ObjectId; // Reference to SalesAssistantDoc
// URLs
managementUrl?: string; // Chatwoot management URL
messengerUrl?: string; // Public bot URL
createdAt: Date;
updatedAt?: Date;
deletedAt?: Date;
}
ExternalChatwootAccountDoc (External Account)
export interface ExternalChatwootAccountDoc {
_id: ObjectId;
tenantId: ObjectId;
name: string;
host: string; // Chatwoot instance URL
accountId: number; // Chatwoot account ID
userAdminAccessToken: string; // Admin API token
createdAt: Date;
deletedAt?: Date;
}
How Inbox Bridges Work
Bridge Creation Flow
User creates bridge in QuotyAI
β
ExternalChatwootService.createExternalChatwootInboxBridge()
β
1. Get external account (host, token from QuotyAI DB)
2. Create MongoDB bridge document (get _id for webhook URL)
β
3. Create agent bot in Chatwoot via API
POST /api/v1/widget/bots
β
4. Update bridge with access token & bot ID
5. Return bridge data to user
Webhook Processing (Incoming Messages)
When Chatwoot receives a message, it sends a webhook to QuotyAI:
// ChatwootWebhookProcessorService
async processWebhookEvent(
payload: ChatwootWebhookPayload,
tenantId: ObjectId,
chatwootInboxBridge: ExternalChatwootInboxBridge
): Promise<void> {
switch (payload.event) {
case 'message_created':
if (chatwootInboxBridge.assistantRole === 'sales') {
await this.unifiedChatbotService.processIncomingMessage(
InboxBridgeType.CHATWOOT, // Channel type
tenantId,
assistantId,
{
payload,
chatwootInboxBridge,
attachments: [...]
},
signal
);
}
break;
// ... handle other events
}
}
Reading Conversations (Proxy Pattern)
// ChatwootConversationsService
async getExternalConversations(
externalAccountId: string,
tenantId: ObjectId,
query: Record<string, unknown>
): Promise<Conversation[]> {
// Get account credentials from QuotyAI DB
const fullAccount = await this.getFullExternalChatwootAccountById(...);
// Direct proxy to Chatwoot API (NOT from QuotyAI DB)
const response = await conversationList({
baseUrl: fullAccount.host,
headers: { 'api_access_token': fullAccount.userAdminAccessToken },
path: { account_id: fullAccount.accountId },
query: query
});
return response.data?.data?.payload || [];
}
Marking Conversations as Reviewed
QuotyAI stores a marker in its own DB:
// ChatwootConversationsService.markConversationAsReviewed()
await this.reviewedConversationsCollection.insertOne({
tenantId,
businessEntityId: new ObjectId(dto.businessEntityId),
accountId: dto.accountId,
inboxId: dto.inboxId,
conversationId: dto.conversationId,
reviewerNotes: dto.reviewerNotes,
reviewedAt: new Date(),
createdAt: new Date()
});
Unified AI Processing
Both native inboxes and inbox bridges use the same AI processing pipeline through channel-specific adapters.
UnifiedSalesAssistantChatbotService
export class UnifiedSalesAssistantChatbotService {
private readonly adapters: Map<UnifiedInboxChannelType, IChatAdapter>;
constructor() {
// Register adapters for each channel type
this.adapters.set(InboxBridgeType.CHATWOOT, this.chatwootAdapter);
this.adapters.set(NativeInboxChannelType.FACEBOOK, this.facebookAdapter);
this.adapters.set(NativeInboxChannelType.TELEGRAM, this.telegramAdapter);
this.adapters.set(StatelessApiChannelType.STATELESS_API, this.statelessApiAdapter);
}
async processIncomingMessage(
channelType: UnifiedInboxChannelType,
tenantId: ObjectId,
assistantId: ObjectId,
input: ChannelInput, // Different for each channel
abortSignal: AbortSignal
): Promise<ProcessedMessageResult> {
const adapter = this.adapters.get(channelType);
// Adapter handles:
// 1. Building AI context (conversation history, etc.)
// 2. Calling LLM
// 3. Sending response back to the channel
}
}
Channel Input Types
Different channels have different input structures:
// For Native Telegram Inbox
export interface TelegramChannelInput {
tenantId: ObjectId;
conversationId: string;
message: string;
chatId: string;
senderId: string;
inbox: NativeInboxDoc;
inboxId: string;
}
// For Native Facebook Inbox
export interface FacebookChannelInput {
tenantId: ObjectId;
conversationId: string;
message: string;
pageId: string;
psid: string;
senderId: string;
inbox: NativeInboxDoc;
inboxId: string;
}
// For Chatwoot Bridge
export interface ChatwootChannelInput {
tenantId: ObjectId;
conversationId: number; // Chatwoot conversation ID
message: string;
payload: ChatwootWebhookPayload; // Full webhook payload
chatwootInboxBridge: ExternalChatwootInboxBridge;
attachments?: ChatwootAttachment[];
}
Integration with Business Entities
Both inbox types connect to business entities for AI context and configuration.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Business Entity β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Pricing rules & formulas β
β β’ Service offerings & catalog β
β β’ Business facts (FAQs, policies) β
β β’ Instructions for AI assistants β
β β’ Configuration (timezone, currency, industry) β
ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββ΄βββββββ
β β
βΌ βΌ
βββββββββΌβββββββββ ββββββββββββββββββ
β Sales β β Management β
β Assistant β β Assistant β
βββββββββ¬βββββββββ ββββββββββ¬ββββββββββ
β β
βΌ βΌ
βββββββββΌβββββββββ ββββββββββββββββββ
β Native β β Inbox β
β Inboxes β β Bridges β
β (Telegram, β β (Chatwoot) β
β Facebook...) β β β
ββββββββββββββββββ ββββββββββββββββββ
AI Assistant Assignment
Native Inbox level:
// NativeInboxDoc.defaultAiReplyAssistantAssignment
{
mode: "fixed_assistant", // or "dynamic_latest"
assistantId: ObjectId, // Reference to SalesAssistantDoc
assistantRole: "sales", // or "accountant"
businessEntityId: ObjectId,
sendingMessageMode: "ai_only", // or "human_only", "ai_with_handover"
handover?: { handedOverAt, handedOverBy, reason }
}
Conversation level (overrides inbox):
// NativeInboxConversationDoc.aiReplyAssistantAssignment
// Same structure - can override inbox-level assignment
Inbox Bridge level:
// ChatwootInboxBridgeDoc
{
assistantRole: "sales",
assistantId: ObjectId,
// ... webhook URL uses apiKeyId for authentication
}
Feature Comparison
| Feature | Native Inbox | Inbox Bridge (Chatwoot) |
|---|---|---|
| Data Ownership | QuotyAI MongoDB | External Chatwoot DB |
| Conversation Storage | Native (MongoDB) | External (Chatwoot API) |
| Message Retrieval | From local DB | Proxy to Chatwoot API |
| Webhook Handling | Receive β Store β Process | Receive β Process (not stored) |
| Supported Channels | Telegram, Facebook, Instagram, WhatsApp, Voice | Any channel Chatwoot supports (10+) |
| Setup Complexity | Direct API integration | Requires Chatwoot instance |
| Attachment Storage | GCS URLs + OCR | Chatwoot handles storage |
| Reviewed Conversations | N/A | Markers stored in QuotyAI |
| Contact Unification | Yes (across channels) | Chatwoot handles contacts |
| Voice Support | Yes (LiveKit) | Via Chatwoot |
| AI Handover | Native implementation | Via Chatwoot UI + API |
| OCR Processing | Built-in (images) | Not available |
| Audit Trail | Full (all in MongoDB) | Limited (QuotyAI stores markers only) |
Technical Implementation Details
Database Collections
| Collection | Purpose | Stores |
|---|---|---|
native-inboxes |
Inbox configurations | tenantId, channel, credentials, webhookUrl, AI assignment |
native-inbox-conversations |
Conversation threads | tenantId, inboxId, contactId, externalId, AI assignment |
native-inbox-contacts |
Unified contact profiles | tenantId, name, email, channelIdentifiers[] |
native-inbox-telegram-messages |
Telegram messages | conversationId, text, events[], attachedImages[], OCR data |
native-inbox-facebook-messages |
Facebook messages | Same structure as Telegram |
voice-inboxes |
Voice inbox configs | tenantId, name, voiceSettings, assistantId |
voice-inbox-sessions |
Voice call sessions | voiceInboxId, roomName, transcript[], callStatus |
chatwoot-inbox-bridges |
Bridge configurations | host, accountId, agentBotId, access tokens, assistant assignments |
external-chatwoot-accounts |
External account credentials | host URL, accountId, admin access token |
external-chatwoot-reviewed-conversations |
Review markers | accountId, inboxId, conversationId, reviewer notes |
Service Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Layer (Hono) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β /omnichannel/inbox-lifecycles/* β Inbox CRUD routes β
β /omnichannel/conversations/* β Conversation routes β
β /omnichannel/webhooks/* β Webhook endpoints β
ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββ΄βββββββ
β β
βΌ βΌ
βββββββββΌβββββββββ ββββββββββββββββββ
β Native β β Chatwoot β
β Services β β Services β
βββββββββ¬βββββββββ€ ββββββββββ¬ββββββββββ€
β Telegram β β External- β
β Facebook β β Chatwoot β
β Lifecycle β β Service β
β Workflow β β Bridge β
β Webhook β β Conversationsβ
ββββββββββββββββββ ββββββββββββββββββ
β β
ββββββββ¬βββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Unified AI Processing β
β UnifiedSalesAssistantChatbotService β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Channel Adapters (Chatwoot, Facebook, Telegram) β
β β’ Skills System (8 modular skills with tools) β
β β’ Dynamic Runner (Sandboxed TypeScript execution) β
β β’ Observability (Full audit trails) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Configuration Example: Creating a Native Telegram Inbox
// POST /omnichannel/inbox-lifecycles/telegram/inboxes/
const response = await fetch('/api/omnichannel/inbox-lifecycles/telegram/inboxes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
telegramApiToken: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
name: 'My Telegram Bot',
businessEntityId: '507f1f77bcf86cd799439011',
apiKeyId: '507f1f77bcf86cd799439012'
})
});
// Response includes:
// - Inbox ID
// - Bot info (id, username)
// - Webhook status
// - Auto-created business entity & assistant (if requested)
Configuration Example: Creating a Chatwoot Inbox Bridge
// POST /omnichannel/inbox-lifecycles/chatwoot/external-instance/inbox-bridges/
const response = await fetch('/api/omnichannel/inbox-lifecycles/chatwoot/external-instance/inbox-bridges/', {
method: 'POST',
body: JSON.stringify({
name: 'My Chatwoot Bridge',
parentExternalAccountId: '507f1f77bcf86cd799439013',
assistantRole: 'sales',
assistantId: '507f1f77bcf86cd799439014',
apiKeyId: '507f1f77bcf86cd799439015'
})
});
// This:
// 1. Creates MongoDB bridge document
// 2. Creates agent bot in Chatwoot
// 3. Configures webhook URL
// QuotyAI does NOT store conversations - only bridge config
Reading Conversations: Native vs Bridge
// Native Inbox: From local MongoDB
// POST /omnichannel/conversations/native-inbox-conversations/search
const nativeConv = await fetch('/api/omnichannel/conversations/native-inbox-conversations/search', {
method: 'POST',
body: JSON.stringify({
page: 1,
limit: 20,
channel: 'TELEGRAM',
sort: 'lastMessageAt',
order: 'desc'
})
});
// Inbox Bridge: Proxied from Chatwoot API
// GET /omnichannel/conversations/chatwoot-bridge-conversations/conversations/:bridgeId
const bridgeConv = await fetch(`/api/omnichannel/conversations/chatwoot-bridge-conversations/conversations/${bridgeId}?limit=20`);
// This calls Chatwoot API internally and returns conversations
When to Use Which?
Choose Native Inboxes When You Want:
- β Complete data ownership β all data in your MongoDB
- β Direct channel integration β no middleware
- β Rich attachment processing β OCR, image analysis
- β Unified contact profiles across channels
- β Voice/chat convergence β same assistant handles both
- β Full audit trails β every message in your DB
- β Custom workflows β full control over processing
Best for: Businesses that want full control, need OCR processing, or handle sensitive data.
Choose Inbox Bridges When You:
- β Already use Chatwoot β leverage existing setup
- β Need broader channel support β Chatwoot supports 10+ channels
- β Want Chatwootβs UI/features β conversation management, agent interface
- β Prefer not to duplicate data β conversations stay in Chatwoot
- β Have complex routing needs β Chatwootβs advanced routing rules
Best for: Businesses already on Chatwoot, or those needing channels QuotyAI doesnβt natively support yet.
The Power of Unified AI
Both approaches provide the same AI assistant capabilities through the unified UnifiedSalesAssistantChatbotService, ensuring consistent customer experience regardless of the underlying inbox type.
Customer sends message via Telegram
β
Native Inbox (stores message)
β OR
Customer sends message via Chatwoot
β
Inbox Bridge (proxies to Chatwoot)
β
ββββββββββββββββββββββββββββββββββ
β Same AI Processing Pipeline β
β β’ 80+ languages β
β β’ 1-2 second response time β
β β’ Deterministic pricing β
β β’ Handover to human agents β
ββββββββββββββββββββββββββββββββββ
β
Customer receives consistent, high-quality response
Summary
QuotyAIβs dual inbox architecture gives you the flexibility to choose the right approach:
| Consideration | Recommendation |
|---|---|
| Data sovereignty matters | Use Native Inboxes |
| Already on Chatwoot | Use Inbox Bridges |
| Need OCR/attachment processing | Use Native Inboxes |
| Need 10+ channels fast | Use Inbox Bridges |
| Voice + chat convergence | Use Native Inboxes |
| Want simple setup | Either β both are straightforward |
Both approaches are first-class citizens in QuotyAI, with full AI assistant integration, business entity support, and comprehensive observability.