Contact Mapping
This document explains how to map external platform user identities to CRM contacts and keep that mapping consistent over time.
Why Contact Mapping Is Necessary
The CRM identifies customers as Contacts with CRM-assigned UUIDs. The external platform identifies users with platform-specific IDs (e.g. Telegram's chat_id).
Your third-party app must maintain a local mapping table (platform_user_id → crm_contact_id) because:
- The CRM does not provide a way to search contacts by an arbitrary external ID.
- You need the
contact_idwhen creating conversations and messages. - You need the
platform_user_idwhen forwarding agent replies.
Contact Lifecycle
Incoming message from new user
↓
Platform user not in your DB?
↓ yes
Create CRM contact → save mapping
↓
Use contact_id for all CRM API calls
↓
(Later) Optionally enrich contact with platform profile dataCreating a Contact
When a platform user sends their first message and you don't have them in your DB yet:
CRM API Call
POST /v1/contacts
Authorization: Bearer {workspace_access_token}
Content-Type: application/json
{
"name": "Telegram User"
}Use the platform's display name if you have it. If you don't (which is common before you've called the platform's profile API), use a placeholder like "Unknown User".
Response (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000"
}Saving the Mapping
Immediately after creating the contact:
await db.saveContactMapping({
platformUserId: platformUserId, // e.g. "Uf1234567890"
contactId: resp.id, // e.g. "ctc_abc123"
workspaceId: workspaceId,
displayName: displayName,
createdAt: new Date(),
});Looking Up a Contact
On every subsequent message from the same user:
async getOrCreateContact(
platformUserId: string,
workspaceId: string,
accessToken: string,
displayName: string
): Promise<string> {
// 1. Check local DB first - avoids CRM API call on hot path
const contactId = await this.db.getContactId(platformUserId, workspaceId);
if (contactId) {
return contactId;
}
// 2. Not found - create in CRM
const resp = await this.crmClient.createContact(accessToken, {
name: displayName,
});
// 3. Save mapping
await this.db.saveContactMapping(platformUserId, workspaceId, resp.id, displayName);
return resp.id;
}Enriching a Contact
Most platforms provide a profile API that returns the user's display name and profile picture. You can enrich the CRM contact with this data either at creation time or asynchronously.
Fetching from the Platform
// Example: fetch Telegram profile
const profile = await telegramClient.getProfile(telegramToken, platformUserId);
// profile.displayName, profile.pictureUrlUpdating the CRM Contact
PATCH /v1/contacts/{contact_id}
Authorization: Bearer {workspace_access_token}
Content-Type: application/json
{
"name": "Kasem Jaidee"
}Setting Custom Attributes
If your workspace has a predefined custom attribute for the platform user ID (e.g. telegram_user_id), you can store it on the contact. Custom attribute keys must be predefined in the workspace - you cannot create new keys via the API.
PATCH /v1/contacts/{contact_id}/custom-attributes
Authorization: Bearer {workspace_access_token}
Content-Type: application/json
{
"telegram_user_id": "Uf1234567890"
}Requires contact:write scope.
Reverse Lookup (CRM → Platform)
When an agent replies (you receive a message.created webhook event), you need the platform_user_id to push the message. Use the conversation_id as the lookup key:
// From conversationId → platformUserId
const platformUserId = await this.db.getPlatformUserId(conversationId);This relies on the conversation mapping table having been populated when conversation.created fired (see Incoming Messages).
Handling Contact Not Found During Reverse Lookup
If GetPlatformUserID returns nothing for a conversation_id, it likely means:
- The conversation was created manually by an agent in the CRM (not via your third-party app).
- Your DB mapping was never saved (e.g.
conversation.createdwebhook event was missed).
In this case, skip forwarding the message - you have no way to know which platform user to push to. Log the incident.
Data Model
Note: This schema is provided as an example/guideline. You may adapt it to your specific requirements, database system, and naming conventions.
-- Bidirectional mapping: platform user ↔ CRM contact
CREATE TABLE contact_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
platform_user_id TEXT NOT NULL,
contact_id TEXT NOT NULL, -- CRM contact UUID
workspace_id TEXT NOT NULL,
display_name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (platform_user_id, workspace_id)
);
CREATE INDEX idx_contact_mappings_contact_id ON contact_mappings(contact_id, workspace_id);The UNIQUE(platform_user_id, workspace_id) constraint ensures you don't accidentally create duplicate CRM contacts for the same platform user in the same workspace.
Edge Cases
| Scenario | How to Handle |
|---|---|
| User messages from two different platform accounts | They will be two separate contacts in the CRM. No merging is done automatically. |
| CRM contact is deleted | Your DB still holds the old contact_id. The CRM will return 404. Create a new contact and update the mapping. |
| Platform user changes display name | Update the CRM contact via PATCH /v1/contacts/{id} periodically or on next message. |
| Same user across multiple workspaces | The mapping is per (platform_user_id, workspace_id) - contacts are isolated per workspace. |