Back to insights
Salesforce IntegrationWhatsAppMeta Cloud API

WhatsApp Integration in Salesforce

Integrate WhatsApp with Salesforce using Meta Cloud API, Apex, Lightning Web Components, and webhooks for real-time messaging.

Deerak Kumar T12 min read

Salesforce Developer

WhatsApp Integration in Salesforce cover infographic

Covers: WhatsAppInbox LWC · WhatsAppInboxService · WhatsAppUtils · Quick Reference

1. Abstract

This document covers the standalone WhatsApp Inbox component and its service layer. Where Part 1 focused on the inbound webhook and the record-page chat widget, this document covers the two-panel WhatsAppInbox LWC that shows all conversations in one place, the WhatsAppInboxService Apex controller that powers it, and the shared WhatsAppUtils utility class used across the integration.

Components Covered

  • WhatsAppInbox LWC — two-panel inbox showing all conversations and message threads
  • WhatsAppInboxService — Apex controller with 5-step conversation aggregation
  • WhatsAppUtils — shared send and phone-lookup methods used by all classes
  • Known limitations — security gaps, SOQL governor risks, performance notes

2. Solution

2.1 WhatsAppInbox — Two-Panel Layout

WhatsAppInbox is a standalone LWC placed on any App Builder page. It has two panels side by side: a fixed 360px left panel listing all conversations, and a flex-grow right panel showing the selected message thread. The design mirrors WhatsApp Web.

PanelContents
Left — Conversation ListDark green top bar with title and refresh icon, search input, scrollable list of conv-rows showing avatar, resolved name, last-message preview (max 35 chars), and timestamp
Right — Message ThreadWelcome card when nothing is selected. On selection: dark green header with contact name, phone, and a 'View [Lead/Contact]' deep-link, then scrollable message list with date separators, then reply input footer

2.2 Tracked State

PropertyPurpose
conversationsMaster list from getAllConversations()
filteredConversationsSearch-filtered and active-row-highlighted copy used by the template
activeMessagesThread for the selected phone, including synthetic date-separator entries
activeChatPhonePhone of the open conversation — drives all right-panel conditionals
activeChatName / InitialDisplay name and first letter shown in the right-panel header
activeChatRecordId / ObjTypeSalesforce Id and type for the deep-link to Lead or Contact
isLoadingConvs / isLoadingMsgsSpinner flags for left and right panels
isSendingDisables send button while outbound Apex call is in flight

2.3 Client-Side Search

handleSearch() filters filteredConversations in memory — no additional Apex call. Checks both name and phone, case-insensitively. Clearing the input restores the full list.

2.4 Date Separators in the Thread

When messages load, _groupByDate() injects synthetic separator objects between groups from different calendar days. These carry isDateSeparator: true and a human-readable dateLabel: 'Today', 'Yesterday', or DD Month YYYY in en-IN locale. Incoming message bubbles are wrapped in a template if:false={msg.isDateSeparator} guard to prevent the component from trying to render fields that don't exist on a separator object.

2.5 Real-Time Updates via Platform Events

_subscribeEvents() subscribes to /event/WA_Message_Event__e from position -1 on load. On every inbound event:

  • loadConversations() always runs to refresh the left-panel preview text and timestamp.
  • If the event phone's last 10 digits match activeChatPhone, _loadMessages() reloads the full right-panel thread.
Performance note: Reloading the full message list on every Platform Event is simple but not optimal at scale. Consider an incremental append strategy (similar to WhatsAppChat's getSingleMessage pattern) once message volume grows.

2.6 Deep-Link to the CRM Record

When activeChatRecordId is populated, the chat header shows an anchor tag with href = '/' + activeChatRecordId and target='_blank'. This lets agents jump directly to the Lead or Contact without leaving the inbox. The link is hidden when no matching record was found.

2.7 Time Formatting

ConditionDisplay Format
Todayh:mm AM/PM (e.g. 9:45 AM)
YesterdayText 'Yesterday'
OlderDD Mon in en-IN locale (e.g. 01 Jun)

2.8 WhatsAppInboxService — Apex Controller

Controller for WhatsAppInbox. Three AuraEnabled methods and an inner wrapper class.

getAllConversations() — 5-Step Pipeline

Returns one ConversationWrapper per unique CustomerPhone__c, sorted by most recent message.

StepWhat It Does
1 — AggregateGROUP BY CustomerPhone__c → max(CreatedDate) + count. Limit 50 by recency.
2 — Latest message per phoneQuery all messages DESC for those phones. Map phone → most recent message for preview text and outgoing flag.
3 — Name from messagesPrefer Contact__r.Name → Lead__r.Name → CustomerName__c from message records.
4 — Fallback: query all CRMFor unresolved phones: query ALL Contacts and ALL non-converted Leads. Last-10-digit comparison. SOQL risk at scale.
5 — Build wrappersAssemble ConversationWrapper: phone, name, initial, lastMessage, lastDate, msgCount, outgoing, recordId, objType.
SOQL Governor Limit Risk: Step 4 queries all Contacts and all Leads without a row limit. In large orgs this can approach the 50,000-row SOQL query limit. Fix: add an indexed NormalisedPhone__c field to Contact and Lead, populate it with the last 10 digits, and filter on it in Step 4.

getMessagesByPhone(phone)

Returns up to 200 WAMessage__c records using the last-10-digit LIKE pattern, ordered by CreatedDate ASC. The 200-record cap should be revisited for long-lived conversations — cursor-based pagination is the right long-term solution.

sendMessage(phone, content)

Thin wrapper around WhatsAppUtils.sendTextMessage(content, phone). Uses parameter names phone and content rather than toPhone and messageContent as in WhatsAppLWCService, but the behaviour is identical.

ConversationWrapper Inner Class

FieldType / Description
phoneString — raw CustomerPhone__c from the database
nameString — resolved display name, or raw phone if unresolved
initialString — first character of name, uppercased, used for the avatar circle
lastMessageString — MessageContent__c of the most recent message
objTypeString — 'Contact', 'Lead', or 'Unknown'
recordIdString — Salesforce Id of the linked Contact or Lead
outgoingBoolean — true if the most recent message was sent by an agent
msgCountInteger — total messages in this conversation
lastDateDatetime — CreatedDate of the most recent message

2.9 WhatsAppUtils — Shared Methods

sendTextMessage(messageContent, toPhone)

Builds and sends an outbound WhatsApp message via the Meta Graph API. Steps in sequence:

  1. Read the WhatsApp Phone Number ID and Bearer token from Named Credential or Custom Label.
  2. Build JSON: { messaging_product: 'whatsapp', recipient_type: 'individual', to: toPhone, type: 'text', text: { preview_url: false, body: messageContent } }
  3. Set Authorization and Content-Type: application/json headers.
  4. On HTTP 200 — insert WAMessage__c (Outgoing__c = true) and return the record.
  5. On failure — throw exception with HTTP status and response body for the caller to surface.
Security: The Bearer token should be stored in a Named Credential, not a Custom Label. Custom Labels are visible to any admin in Setup. Named Credentials encrypt the value and are the correct pattern for outbound API credentials in Salesforce.

findLeadOrContact(phone)

Strips non-numeric characters, takes the last 10 digits, and queries Contact (by MobilePhone and Phone) then non-converted Lead (same fields). Returns the Salesforce Id of the first match, or null. The webhook calls this after inserting WAMessage__c to populate Lead__c or Contact__c.

3. Step-by-Step Guide

3.1 Using WhatsAppInbox

  1. Open the Lightning App Builder. Drag the whatsAppInbox component onto any App page. Save and Activate.
  2. Navigate to the page. The left panel loads all conversations showing name, last message preview, and timestamp.
  3. Type in the search box to filter by name or phone. The filter runs in memory — no server call. Clear the field to restore the full list.
  4. Click any conversation row. The right panel loads the thread with date separators (Today / Yesterday / DD Mon). A 'View [Lead/Contact]' link appears in the header if a record is linked.
  5. Type in the footer textarea and press Enter to send a reply. Shift+Enter inserts a line break. The sent message appears immediately and the conversation list refreshes.
  6. New inbound messages arrive automatically via Platform Event. Both the conversation list and the open thread update without a page reload.

3.2 Applying the Recommended Fixes

FixAction
Fix 1 — Verify TokenMove 'mysecret2024' from WhatsAppWebhook.doGet() to Custom Label WA_VERIFY_TOKEN. Update code to read Label.WA_VERIFY_TOKEN.
Fix 2 — Bearer TokenStore the Meta Bearer token in a Named Credential instead of a Custom Label. Update WhatsAppUtils to read from the Named Credential.
Fix 3 — SOQL LimitAdd NormalisedPhone__c (indexed Text field) to Contact and Lead. Populate with last 10 digits of phone. Filter on it in getAllConversations() Step 4.
Fix 4 — Debug LogsWrap all System.debug() calls in WhatsAppLWCService with: if (CustomSettings__c.getInstance().Debug_Mode__c) { ... }. Or delete before go-live.

4. Conclusion

4.1 Summary

WhatsAppInbox provides a familiar two-panel inbox experience directly inside Salesforce, with client-side search, date-grouped threads, and one-click navigation to the linked CRM record. The underlying service layer handles complex name resolution across multiple fallback levels. The items below should be addressed before deploying to large production orgs.

4.2 Strengths

  • Two-panel inbox mirrors WhatsApp Web — immediately familiar to agents
  • Client-side search is fast with no additional Apex calls
  • Date separators make long threads readable
  • Deep-link to Lead or Contact from within the inbox
  • getAllConversations resolves names at three fallback levels: linked record, message field, direct CRM lookup
  • Real-time updates via Platform Events — no page refresh needed

4.3 Areas to Improve

  • getAllConversations() Step 4 queries all Contacts and Leads — SOQL governor limit risk at scale
  • Full message reload on every Platform Event — not efficient at high volume
  • getMessagesByPhone() hard-capped at 200 records — needs cursor-based pagination
  • Inbound media stored as placeholder strings — no download from Meta media API
  • No pagination in the current implementation
  • Bearer token in Custom Label should move to Named Credential

5. Screenshots & Visual Reference

5.1 WhatsAppInbox — Two-Panel Layout Description

UI ElementVisual Description
Left top barDark green (#075E54) bar with 'WhatsApp' title in white and a refresh icon on the right
Search barLight grey strip below the top bar with a white pill-shaped input field
Conversation rowEach row: teal gradient avatar circle with initial, contact name bold on the left, timestamp right-aligned, last message preview in grey below the name. Active row has a light grey background.
Right welcome screenShown before any conversation is selected — WhatsApp icon, 'WhatsApp Inbox' heading, 'Select a conversation' sub-text
Chat headerDark green bar with circular avatar, contact name, phone number, and a 'View Contact ↗' pill link
Date separatorCentred pill label (Today / Yesterday / DD Mon YYYY) between message groups
Outgoing bubbleLight green (#D9FDD3), right-aligned, right-tail, double-tick icon bottom-right
Incoming bubbleWhite, left-aligned, left-tail, customer name in teal above the message text
Reply footerLight grey strip with pill-shaped textarea and circular green send button

5.2 Platform Event Flow — Text Diagram

Inbound message arrives
  → WhatsAppWebhook.doPost()
  → upsert WAMessage__c
  → WhatsAppUtils.findLeadOrContact() → update Lead__c / Contact__c
  → insert WA_Message_Event__e
  → empApi subscription (LWC)
      → WhatsAppChat._handleEvent()
          → getSingleMessage() → append to messages[]
      → WhatsAppInbox._handleEvent()
          → loadConversations() (always — refreshes left panel)
          → _loadMessages() (only if active phone matches)

5.3 Full Component Quick Reference

ComponentTypeCalled ByKey Method / Note
WhatsAppWebhookApex RESTMeta PlatformdoPost() — receive & validate inbound message
WhatsAppUtilsApex UtilityWebhook + both servicessendTextMessage() / findLeadOrContact()
WAMessage__cCustom ObjectAll Apex classesUpsert on MessageID__c (External Id)
WA_Message_Event__ePlatform EventWebhook on every insertTriggers real-time LWC update via empApi
WhatsAppChatLWCLead / Contact pageIncremental append via getSingleMessage()
WhatsAppLWCServiceApex ServiceWhatsAppChat LWClistAllMessages() / sendTextMessage()
WhatsAppInboxLWCAny App Builder pageFull reload via _loadMessages() on event
WhatsAppInboxServiceApex ServiceWhatsAppInbox LWCgetAllConversations() / getMessagesByPhone()
Topics:WhatsAppMeta Cloud APISalesforce

Ready to accelerate your digital transformation?

Partner with Subsel to build modern CRM, AI-enabled automation, and enterprise systems that scale.