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

If your business talks to customers on WhatsApp, Instagram, and Telegram, you probably already have three different "versions" of the same person. On WhatsApp they're a phone number. On Instagram they're an IGSID (Instagram-Scoped ID). On Telegram they're a numeric

  • API access to the three channels: a WhatsApp Business Platform account (via Meta or a BSP), an Instagram Professional account connected to a Facebook Page with instagram_manage_messages permission, and a Telegram bot token from BotFather.

  • A backend that can receive webhooks (Node.js/Express, Python/FastAPI, or similar) with HTTPS endpoints.

  • A database that supports flexible schema evolution — Postgres with JSONB columns works well; a document store like MongoDB also fits.

  • Basic familiarity with webhook signature verification (each platform signs its payloads differently).

  • A queue (SQS, RabbitMQ, or even Postgres-as-a-queue) to decouple ingestion from profile-matching, since matching logic can get slow under load.

The Core Problem: Three Identity Systems, Zero Overlap
Picture a customer named Amara. She messages you on Instagram to ask about a delayed order, then two days later texts your WhatsApp number about the same order because it's faster. Without a unified profile:

  • Your Instagram bot has no memory of the WhatsApp conversation.

  • Your support agent on WhatsApp re-asks for the order number Amara already gave on Instagram.

  • Any automation ("send a satisfaction survey after resolution") fires twice, once per channel, annoying her further.

The fix isn't "pick one channel." It's building a resolution layer that sits underneath all three channel integrations and exposes one customer_id to everything downstream — your CRM, your analytics, your automation rules.

Each channel's webhook lands in the same ingestion layer, gets normalized into a common event shape, and is handed to a resolution service that decides "have I seen this human before, under a different identity?"

Designing the Unified Profile Schema
The key design decision is separating identities (channel-specific handles) from the profile (the merged human). Never store channel IDs as your primary key — you'll paint yourself into a corner the first time a customer uses a second channel.

{
"customer_id": "cust_8f21ac",
"created_at": "2026-08-02T10:14:00Z",
"display_name": "Amara Chen",
"verified_contact": {
"phone": "+15551234567",
"email": null
},
"identities": [
{
"channel": "whatsapp",
"channel_user_id": "15551234567",
"linked_at": "2026-08-02T10:14:00Z",
"confidence": "verified"
},
{
"channel": "instagram",
"channel_user_id": "179384756201",
"username": "amara.c",
"linked_at": "2026-08-04T09:02:11Z",
"confidence": "high"
},
{
"channel": "telegram",
"channel_user_id": "623910442",
"username": "amarac",
"linked_at": null,
"confidence": "unlinked"
}
],
"attributes": {
"order_ids": ["ORD-90213"],
"language": "en",
"tags": ["vip", "delayed-order"]
}
}

Two fields do most of the work here:

  • identities[] — every channel handle ever seen for this profile, each with a confidence level so downstream systems know how certain the match is.

  • confidence — not every match is equally trustworthy (more on this below). Automations that send money or sensitive data should only trust verified links; a chatbot pulling up order history can act on high confidence.

Identity Resolution: Matching Channels to a Person
Resolution is a ranked set of signals, tried in order of reliability:

  1. Explicit link (verified) — the customer typed the same phone number into a WhatsApp chat and an Instagram chat, or completed an OTP flow that ties an IGSID to a phone number. This is the only tier you should call "verified."

  2. Deterministic match (high) — same phone number appears in Instagram's ig-connected-account field (available if the customer linked accounts), or the customer's Telegram username matches a value already stored against another channel.

  3. Contextual match (medium) — same order number, same support ticket ID, or the customer says "I messaged you on WhatsApp about this" inside an Instagram thread. Worth flagging for a human to confirm, not for full auto-merge.

  4. No match — create a new profile.

async function resolveIdentity(event) {
  const { channel, channelUserId, extractedPhone, extractedOrderId } = event;

  // Tier 1: verified phone match
  if (extractedPhone) {
    const existing = await db.profiles.findOne({
      "verified_contact.phone": extractedPhone
    });
    if (existing) return attachIdentity(existing.customer_id, channel, channelUserId, "verified");
  }

  // Tier 2: deterministic username/ID match
  const usernameMatch = await db.profiles.findOne({
    "identities.username": event.username,
    "identities.channel": { $ne: channel }
  });
  if (usernameMatch) return attachIdentity(usernameMatch.customer_id, channel, channelUserId, "high");

  // Tier 3: contextual match, queued for human review
  if (extractedOrderId) {
    const orderMatch = await db.profiles.findOne({ "attributes.order_ids": extractedOrderId });
    if (orderMatch) {
      await flagForReview(orderMatch.customer_id, channel, channelUserId, extractedOrderId);
      return attachIdentity(orderMatch.customer_id, channel, channelUserId, "medium");
    }
  }

  // No match — new profile
  return createProfile(channel, channelUserId);
}
Enter fullscreen mode Exit fullscreen mode

Order numbers, tracking IDs, and email addresses mentioned in message text are strong contextual signals — a lightweight regex or NER pass over inbound message text (before it hits your bot) is usually enough to extract them without a full NLP stack.

Ingesting Events From Each Channel
Each platform's webhook payload has a different shape, so normalize at the door. Here's a minimal Express handler pattern for the three inbound webhooks:

app.post('/webhooks/whatsapp', verifyMetaSignature, (req, res) => {
  const msg = req.body.entry[0].changes[0].value.messages?.[0];
  if (msg) enqueue(normalize('whatsapp', msg.from, msg));
  res.sendStatus(200);
});

app.post('/webhooks/instagram', verifyMetaSignature, (req, res) => {
  const msg = req.body.entry[0].messaging[0];
  enqueue(normalize('instagram', msg.sender.id, msg));
  res.sendStatus(200);
});

app.post('/webhooks/telegram', verifyTelegramSecret, (req, res) => {
  const msg = req.body.message;
  enqueue(normalize('telegram', String(msg.from.id), msg));
  res.sendStatus(200);
});

function normalize(channel, channelUserId, raw) {
  return {
    channel,
    channelUserId,
    text: extractText(channel, raw),
    timestamp: Date.now(),
    raw
  };
}
Enter fullscreen mode Exit fullscreen mode

Each of the three verify* middlewares matters more than it looks — Meta signs WhatsApp and Instagram payloads with an X-Hub-Signature-256 HMAC, while Telegram relies on a secret token in the URL path or the X-Telegram-Bot-Api-Secret-Token header. Skipping verification means anyone who finds your endpoint URL can inject fake events into a real customer's profile.

Handling Conflicts and Merges
Two situations will break a naive implementation:
Same phone number, different humans. Shared family phones or reissued numbers mean a verified phone match isn't always the same person over time. Store a linked_at timestamp per identity and consider expiring verified status after a configurable window of inactivity (say, 12 months) so a stale link doesn't silently misattribute a new person's messages to someone else's history.
Two profiles need merging after the fact. If your resolution service later discovers that cust_8f21ac and cust_44a1b0 are the same person (e.g., a human agent confirms it), merges should be append-only and reversible:

async function mergeProfiles(primaryId, secondaryId, mergedBy) {
  const secondary = await db.profiles.findOne({ customer_id: secondaryId });

  await db.profiles.updateOne(
    { customer_id: primaryId },
    {
      $push: { identities: { $each: secondary.identities } },
      $set: { [`merge_history.${secondaryId}`]: { mergedBy, mergedAt: new Date() } }
    }
  );

  await db.profiles.updateOne(
    { customer_id: secondaryId },
    { $set: { merged_into: primaryId, active: false } }
  );
}
Enter fullscreen mode Exit fullscreen mode

Never hard-delete the secondary profile — keeping it as a soft-redirect (merged_into) means any system still holding the old customer_id in cache or in an old support ticket can be resolved forward without breaking references.

Privacy and Retention Considerations
A few design choices to bake in from day one rather than retrofit later:

  • Data minimization. Don't pull more profile data from each platform's API than your use case needs — fetching a full Instagram profile (bio, follower count) for every inbound DM is usually unnecessary and adds compliance surface area.

  • Right-to-erasure support. Structuring identities as an array on one profile document (rather than scattered across per-channel tables) makes it straightforward to delete or anonymize one customer_id and every identity attached to it in a single operation.

  • Consent per channel. A customer messaging you on Instagram hasn't necessarily consented to being contacted on WhatsApp, even if you've matched the identities. Keep a contactable_channels list separate from identities so matching for context doesn't silently become permission to message.

  • Audit trail on merges. Keep the merge_history field shown above — when (not if) a merge turns out to be wrong, you need to know who approved it and unwind it cleanly.

Conclusion
A single customer profile isn't a single database table it's a resolution pipeline: normalize events from each channel, score identity matches by confidence tier, store identities as a list rather than a key, and treat merges as reversible operations with an audit trail. Get that foundation right and everything downstream — your support inbox, your chatbot's memory, your analytics gets simpler instead of harder as you add more messaging channels.
The channels will keep multiplying (WhatsApp, Instagram, Telegram today; whatever's next tomorrow). The identity-resolution layer is the piece that keeps your customer data sane no matter how many of them you plug in.

Top comments (0)