DEV Community

Cover image for Designing a Single Customer Profile Across WhatsApp, IG, and Telegram
BotSailor
BotSailor

Posted on

Designing a Single Customer Profile Across WhatsApp, IG, and Telegram

A customer messages you on WhatsApp on Monday, DMs your Instagram on Wednesday, and pings your Telegram bot on Friday. Your system sees three different strangers.

This post shows you how to fix that. You'll learn how to design a single customer view across WhatsApp, Instagram, and Telegram: the data model, the identity resolution logic, the merge strategy, and the consent and CRM-sync details that most tutorials skip.

What Is a Unified Customer Profile?

A unified customer profile is one record that represents one real person, no matter how many channels or identifiers they use to reach you. It combines identities, contact details, conversation history, and consent state into a single unified customer view, often called a 360 customer view.

Without it, every channel becomes its own silo:

  • Support asks the same question the customer already answered on another app.

  • Marketing sends the same offer three times.

  • Analytics counts one person as three "users."

  • AI agents answer without context because they can't see the full history.

The goal of any customer database platform, whether you buy it or build it, is to keep unified customer data in one place and make it the source of truth for every team and every automated agent.

What Is Customer Identity Resolution?

Customer identity resolution is the process of deciding that two or more identifiers (a WhatsApp number, an Instagram-scoped ID, a Telegram user ID) belong to the same person. It is the core of any unified customer database.

How does customer identity resolution work?

There are two approaches:

Deterministic matching: link records on an exact, trustworthy key such as a verified phone number or email. It is precise and auditable.
Probabilistic matching: link records by scoring similarity (name, timing, behavior). It is flexible but produces false positives.

Tip: Start deterministic-only. A wrongly merged profile (two people combined into one) is far more damaging than a missed merge (one person split in two). You can always merge later. Un-merging is painful.

The takeaway: the channel-scoped ID is your primary key per channel, and phone or email are the bridges between channels. Users hand you a bridge only when they choose to, for example by typing an email in chat, sharing a Telegram contact, or filling a form.

Architecture: How to Build an Omnichannel Customer Profile

An omnichannel customer profile sits at the end of a simple pipeline:

  • Ingestion: webhooks from WhatsApp, Instagram, and Telegram.

  • Normalization: convert each payload into one common event shape.

  • Identity resolution: map the event to a customer, creating or merging as needed.

  • Profile store: the unified customer database.

  • Sync: push changes to your CRM, analytics, and AI agents.

How to Create a Unified Customer Profile (Step by Step)
Step 1: Separate identities from profiles

The most important design decision: a customer is not an identity. A customer has many identities. Model them as two tables.

CREATE TABLE customers (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  display_name  TEXT,
  email         TEXT,
  phone_e164    TEXT,
  merged_into   UUID REFERENCES customers(id),
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE identities (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_id   UUID NOT NULL REFERENCES customers(id),
  channel       TEXT NOT NULL CHECK (channel IN ('whatsapp','instagram','telegram')),
  external_id   TEXT NOT NULL,
  handle        TEXT,
  verified      BOOLEAN NOT NULL DEFAULT false,
  first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (channel, external_id)
);

CREATE INDEX idx_customers_phone ON customers (phone_e164);
CREATE INDEX idx_customers_email ON customers (lower(email));
Enter fullscreen mode Exit fullscreen mode

The UNIQUE (channel, external_id) constraint is your safety net against duplicate identities when two webhooks arrive at the same moment. The merged_into column lets you soft-merge profiles without losing history.

Step 2: Normalize every channel into one event shape

Don't let channel-specific payloads leak into your business logic. Convert them at the edge.

type Channel = "whatsapp" | "instagram" | "telegram";

interface NormalizedMessage {
  channel: Channel;
  externalId: string;
  displayName?: string;
  handle?: string;
  phone?: string;   // E.164, only when the platform gave it to us
  email?: string;
  text?: string;
  receivedAt: Date;
}

export function fromWhatsApp(payload: any): NormalizedMessage[] {
  const out: NormalizedMessage[] = [];
  for (const entry of payload.entry ?? []) {
    for (const change of entry.changes ?? []) {
      const value = change.value ?? {};
      for (const msg of value.messages ?? []) {
        const contact = (value.contacts ?? []).find((c: any) => c.wa_id === msg.from);
        out.push({
          channel: "whatsapp",
          externalId: msg.from,
          displayName: contact?.profile?.name,
          phone: "+" + msg.from,
          text: msg.text?.body,
          receivedAt: new Date(Number(msg.timestamp) * 1000),
        });
      }
    }
  }
  return out;
}

export function fromInstagram(payload: any): NormalizedMessage[] {
  return (payload.entry ?? []).flatMap((entry: any) =>
    (entry.messaging ?? [])
      .filter((evt: any) => evt.message && !evt.message.is_echo)
      .map((evt: any) => ({
        channel: "instagram" as const,
        externalId: evt.sender.id, // IGSID; fetch name/username via the profile API later
        text: evt.message.text,
        receivedAt: new Date(evt.timestamp),
      }))
  );
}

export function fromTelegram(update: any): NormalizedMessage[] {
  const msg = update.message;
  if (!msg?.from) return [];

  // A user can share someone else's contact, so only trust it if it's their own.
  const ownContact = msg.contact && msg.contact.user_id === msg.from.id;

  return [{
    channel: "telegram",
    externalId: String(msg.from.id),
    handle: msg.from.username,
    displayName: [msg.from.first_name, msg.from.last_name].filter(Boolean).join(" "),
    phone: ownContact ? normalizePhone(msg.contact.phone_number) : undefined,
    text: msg.text,
    receivedAt: new Date(msg.date * 1000),
  }];
}
Enter fullscreen mode Exit fullscreen mode

Note: Webhook payload fields change between API versions. Verify these snippets against the current Meta and Telegram docs before shipping.

Step 3: Resolve identity on every event

This is where you unify customer data across channels. The order of checks matters: exact channel identity first, then deterministic bridges, and a new customer only as a last resort.

// Pseudocode: `tx` is your transactional data layer.
export async function resolveCustomer(tx: Tx, m: NormalizedMessage): Promise<string> {
  // 1. Have we seen this exact channel identity before?
  const known = await tx.identities.find(m.channel, m.externalId);
  if (known) return followMergePointer(tx, known.customerId);

  // 2. Do we have a deterministic bridge (verified phone or email)?
  const candidates = await tx.customers.findByKeys({ phone: m.phone, email: m.email });

  let customerId: string;
  if (candidates.length === 1) {
    customerId = candidates[0].id;
  } else if (candidates.length > 1) {
    customerId = await mergeCustomers(tx, candidates.map((c) => c.id));
  } else {
    // 3. No match: create a new profile.
    customerId = await tx.customers.create({ displayName: m.displayName, phone: m.phone });
  }

  await tx.identities.insert({
    customerId,
    channel: m.channel,
    externalId: m.externalId,
    handle: m.handle,
    verified: Boolean(m.phone || m.email),
  });
  return customerId;
}
Enter fullscreen mode Exit fullscreen mode

To identify the same customer across channels, you need a bridge event. Common ones:

The customer shares a phone number or email in chat, and you extract and verify it.
A Telegram user taps a "Share my contact" button.
The customer clicks a deep link from one channel into another (for example, a wa.me link or a Telegram start parameter carrying a signed one-time token).
The customer logs in or fills a form on your site, which ties multiple identities to one account.
Step 4: Merge profiles safely

Merging is the most dangerous operation in the system. Make it transactional, reversible, and logged.

BEGIN;

-- Move every identity and conversation to the surviving profile
UPDATE identities     SET customer_id = :survivor WHERE customer_id = :loser;
UPDATE conversations  SET customer_id = :survivor WHERE customer_id = :loser;

-- Leave a pointer instead of deleting the old record
UPDATE customers SET merged_into = :survivor, updated_at = now() WHERE id = :loser;

-- Keep an audit trail so the merge can be reviewed or reversed
INSERT INTO merge_log (survivor_id, loser_id, reason, merged_at)
VALUES (:survivor, :loser, 'verified_phone_match', now());

COMMIT;
Enter fullscreen mode Exit fullscreen mode

Simple survivorship rules that work well:

The oldest profile becomes the survivor, which keeps IDs stable for downstream systems.
For conflicting fields, the most recently verified value wins.
Never overwrite a verified value with an unverified one.
Step 5: Track consent per channel

A unified profile does not mean unified permission. Consent to receive WhatsApp messages says nothing about Instagram or Telegram, and each platform has its own messaging rules, including time windows for replying to customers.

CREATE TABLE consents (
  customer_id UUID NOT NULL REFERENCES customers(id),
  channel     TEXT NOT NULL,
  purpose     TEXT NOT NULL,   -- e.g. 'support', 'marketing'
  status      TEXT NOT NULL CHECK (status IN ('granted','revoked')),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (customer_id, channel, purpose)
);
Enter fullscreen mode Exit fullscreen mode

Warning: When you merge two profiles, merge consent conservatively. If either profile revoked consent for a channel and purpose, treat the merged profile as revoked until you have a fresh opt-in.

How to Connect Social Media Customer Data to a CRM

Once you have a single customer view, the last mile is pushing it to your CRM. Treat your unified database as the source of truth and the CRM as a downstream consumer.

Store your internal customer.id as an external ID on the CRM contact.
Upsert CRM contacts by that ID first, then fall back to email or phone.
Emit events such as customer.created, customer.updated, and customer.merged, and deliver them through an outbox table so nothing is lost.

{
  "type": "customer.merged",
  "survivor_id": "b2f1c9e0-0000-0000-0000-000000000001",
  "merged_ids": ["a7d3f4c1-0000-0000-0000-000000000002"],
  "occurred_at": "2026-09-21T09:30:00Z"
}
Enter fullscreen mode Exit fullscreen mode

The customer.merged event matters most. Without it, your CRM will keep two contacts for one person long after your database has fixed the problem.

Why a Unified Customer View Matters for AI Agents

If you're building agentic or multi-agent systems, the profile is more than a reporting convenience. It is the shared memory every agent reads from:

A support agent sees purchase history and open tickets from all channels.
A sales agent knows what the customer asked about on Instagram before continuing on WhatsApp.
A routing agent can pick the right channel based on consent and past responsiveness.

Agents that share one unified customer profile stay consistent. Agents that each keep their own per-channel context contradict each other.

Common Pitfalls
Using usernames as keys. Telegram and Instagram handles change. Use numeric or scoped IDs.
Trusting unverified phone numbers. Only match on values the platform or the user has verified.
Merging on fuzzy name matches. Two people named "Sam Lee" are not the same customer.
Hard-deleting the losing profile. Keep a merge pointer and audit log.
Ignoring privacy law. A unified database concentrates personal data, so plan retention, access control, and deletion requests from day one.
Frequently Asked Questions
How do I create a single customer view across multiple messaging apps?

Give each channel identity its own row, link identities to one customer record, and resolve identities deterministically using verified phone numbers or emails. Merge profiles transactionally and log every merge.

How do I combine WhatsApp, Instagram, and Telegram data?

Normalize each platform's webhook payload into one common event shape, then run every event through the same identity resolution step. This is the core of any approach to how to integrate WhatsApp, Instagram, and Telegram into one system.

How do I track customers across multiple messaging apps if they never share a phone or email?

You can't merge them reliably, and you shouldn't guess. Create bridge moments instead: deep links with signed tokens, login prompts, or a friendly request for an email address.

What are customer data solutions, and do I need one?

Customer data solutions are tools and platforms that collect, unify, and activate customer data. If you have more than a couple of channels and a CRM, you need the capabilities, whether you build them or buy them.

Conclusion

Here are the key takeaways:

  • A unified customer profile is one record with many identities, not one identity per channel.

  • Use deterministic identity resolution first, and merge only on verified keys.

  • Normalize every channel at the edge so your business logic stays channel-agnostic.

  • Make merges transactional, logged, and reversible.

  • Track consent per channel, and sync merge events to your CRM.

  • Treat the unified profile as shared memory for your AI agents.

Top comments (0)