Technical docs
Contacts and Channels
Overview
Support reference for how customer identity is linked across Omni, public webchat, API threads, Telegram, and internal chat.
Core model
- `Contact` is the stable person record.
- `ContactChannel` is the per-channel identity for that contact.
- `Conversation` always attaches to a `contact_channel_id`, not directly to `contact_id`.
Stored channel types
- The `ContactChannel` model currently defines:
- `api`
- `web_chat`
- `skill_chat`
- `employee_chat`
- `telegram`
- Frontend Omni types still mention `test_chat`, but the backend channel enum does not.
Identity key and profile fields
- Contact-channel uniqueness is enforced by:
- `organization_id + channel + external_id`
- Key `ContactChannel` fields used in current code:
- `contact_channel_id`
- `contact_id`
- `channel`
- `external_id`
- `auto_name`
- `first_name`
- `last_name`
- `phone`
- `email`
- `current_conversation_id`
- `auto_name` is generated when a channel record is first created.
- Phone and email are normalized on create/update.
Omni customer mapping
- Omni customer identity is derived from the backend `contact_channel` payload on the conversation.
- The current conversation DTO also includes `participants[]`, and the frontend now treats that participant list as the source of truth when resolving the primary contact channel for a thread.
- Display name resolution is:
- `first_name + last_name`
- otherwise `auto_name`
- otherwise `Unknown Customer`
- The current Omni customer summary uses `contact_channel` data for:
- name
- phone
- For group-capable conversations, the frontend currently picks the first active contact-channel participant as the primary customer summary until it supports a full roster UI.
Public webchat linkage
- Public webchat session identity is resolved in this order:
- valid `webchat-token`
- `contact.external_id` from the request
- generated UUID
- Once a public message flow resolves an external ID, `UpsertWebchatConversation` upserts a `web_chat` contact channel for:
- organization
- channel = `web_chat`
- that external ID
- Re-sending the same `contact.external_id` reuses the same `ContactChannel`.
- Supplying profile fields (`first_name`, `last_name`, `phone`, `email`) updates the existing `ContactChannel` instead of creating a second one.
- If a request includes both a valid `webchat-token` and an explicit `contact.external_id`, the backend now rejects the request when those two identities do not match.
Conversation linkage and reuse
- All conversations belong to one `contact_channel_id`.
- For public webchat:
- default behavior is single-conversation reuse per resolved `web_chat` contact channel and AI employee scope
- if `multi_conversations` is true:
- explicit `conversation_id` reuses that conversation
- no `conversation_id` creates a new conversation
- `GET /api/public/webchat/conversations` lists all conversations for a resolved `web_chat` contact channel.
- Public webchat conversation list and message reads now scope membership through active contact-channel participants, so group-capable conversation queries do not rely only on the legacy conversation-level `contact_channel_id` column.
`current_conversation_id` behavior
- `current_conversation_id` exists on `ContactChannel`.
- In the current codebase it is actively used and updated for internal chat conversation resolution.
- Internal chat resolution order is:
- explicit `create_new_conversation`
- explicit `conversation_id`
- `current_conversation_id`
- latest conversation for that contact channel
- Public webchat upsert does not currently use or update `current_conversation_id`.
- Omni queue loading also does not rely on `current_conversation_id`.
Internal chat external IDs
- Internal chat still uses the same `Contact` and `ContactChannel` system.
- Internal chat uses generated external IDs for skill and AI teammate chat channels.
- There is no separate stored `ally_chat` contact-channel type in the current backend model.
- The skill-chat prefix is still `test__` in `ContactService`, even though the stored channel type is `skill_chat`.
Telegram linkage
- Telegram is now a first-class `ContactChannel.channel` value.
- AI teammate Telegram bots use employee-scoped bot credentials stored on the AI teammate record:
- `telegram_bot_id`
- `telegram_bot_token`
- Employee Telegram management routes are:
- `GET /api/organizations/{orgId}/employees/{employee_id}/telegram`
- `POST /api/organizations/{orgId}/employees/{employee_id}/telegram`
- `DELETE /api/organizations/{orgId}/employees/{employee_id}/telegram`
- `PUT /api/organizations/{orgId}/employees/{employee_id}/telegram/webhook`
- Telegram conversation external chat identity currently resolves as:
- private or non-topic chat: `String(message.chat.id)`
- topic message: `"{message.chat.id}:{message.message_thread_id}"`
- That means separate Telegram topics in the same group can map to separate Alloy conversation external IDs.
- Telegram conversation titles are auto-managed only for non-private chats:
- private chats keep `title = null`
- group and supergroup chats use the current Telegram chat title when available
- topic-aware titles append the topic name when the inbound Telegram payload includes that topic-creation context
- Existing Telegram group conversations are updated to the latest available auto title when new inbound messages provide title metadata.
- Incoming Telegram messages currently support:
- text
- photo
- document
- audio
- voice
- video
- video note
- animation
- sticker
- The webhook processes at most `5` Telegram attachments per inbound message.
- Each Telegram attachment is capped at `10 MB`; oversized items are skipped and the rest of the message still continues.
- Incoming Telegram messages without text can still be accepted when at least one supported attachment survives validation.
- For Telegram-triggered workflow runs, if the inbound message replies to a text message, the workflow input `message` is prefixed as `reply to [[original text]]: {new text}`.
- Workflow execution only forwards image attachments into workflow `inputData.attachments`; non-image Telegram files stay attached to the saved message but are not passed into the AI input payload.
- Outbound Telegram text replies are split into multiple sequential Telegram messages when the text exceeds Telegram's `4096` character message limit.
- During AI reply generation for Telegram conversations, the bot sends Telegram `typing` chat actions while the workflow is running.
- For system AI employees that are owned by a specific Alloy user, inbound Telegram bot messages are ignored unless the Telegram sender matches that owner's linked `telegramAuth.telegram_id`.
Search behavior
- `ConversationService.searchByContactChannel(...)` is scoped to one contact channel at a time.
- Search sources are:
- conversation title via `ILIKE`
- message content via full-text search
- Title hits are returned before message hits.
- Search can also be scoped by `aiEmployeeId`.
Support checks
- If a public widget user unexpectedly forks into a second identity:
- inspect `channel + external_id`
- inspect whether the widget changed `contact.external_id`
- inspect whether the stored `webchat-token` belongs to a different external ID
- inspect whether the request mixed a valid `webchat-token` with a different explicit `contact.external_id`
- If an internal chat thread reopens the wrong conversation:
- inspect `current_conversation_id`
- inspect `contact_channel_id`
- inspect the generated external ID format for that channel
- for Ally, confirm which per-user Ally employee was resolved
- If Omni shows the wrong customer name:
- inspect `participants[]`
- inspect `first_name`
- inspect `last_name`
- inspect `auto_name`
- If a Telegram customer says a file was sent but the workflow did not use it:
- inspect the inbound message attachments
- confirm whether the file was an image or a non-image attachment
- confirm whether the attachment exceeded `10 MB`
- remember that only image attachments are forwarded into workflow input data
- If a Telegram topic or group message lands in the wrong conversation:
- inspect the resolved external chat ID
- inspect whether the inbound message carried `message_thread_id`
- If a Telegram group thread shows an outdated or missing title in Omni:
- inspect the latest inbound Telegram payload for `chat.title`
- inspect whether topic context was present in the inbound payload
- remember that private Telegram chats intentionally keep no conversation title
- If a Telegram AI reply seems to reference an earlier quoted message unexpectedly:
- inspect whether the customer used Telegram reply-to on a text message
- inspect the workflow input `message` payload for the injected `reply to [[...]]:` prefix
- If a system employee Telegram bot ignores a message unexpectedly:
- inspect whether the AI employee is a system employee with `owned_by_user_id`
- inspect the owner's linked Telegram account ID versus the inbound sender Telegram ID