Developer Docs Logo
Customer Chat

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_id when creating conversations and messages.
  • You need the platform_user_id when 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 data

Creating 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.pictureUrl

Updating 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:

  1. The conversation was created manually by an agent in the CRM (not via your third-party app).
  2. Your DB mapping was never saved (e.g. conversation.created webhook 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

ScenarioHow to Handle
User messages from two different platform accountsThey will be two separate contacts in the CRM. No merging is done automatically.
CRM contact is deletedYour DB still holds the old contact_id. The CRM will return 404. Create a new contact and update the mapping.
Platform user changes display nameUpdate the CRM contact via PATCH /v1/contacts/{id} periodically or on next message.
Same user across multiple workspacesThe mapping is per (platform_user_id, workspace_id) - contacts are isolated per workspace.

On this page