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
  • email
  • 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

Start building your AI team