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

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.
| Panel | Contents |
|---|---|
| Left — Conversation List | Dark 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 Thread | Welcome 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
| Property | Purpose |
|---|---|
| conversations | Master list from getAllConversations() |
| filteredConversations | Search-filtered and active-row-highlighted copy used by the template |
| activeMessages | Thread for the selected phone, including synthetic date-separator entries |
| activeChatPhone | Phone of the open conversation — drives all right-panel conditionals |
| activeChatName / Initial | Display name and first letter shown in the right-panel header |
| activeChatRecordId / ObjType | Salesforce Id and type for the deep-link to Lead or Contact |
| isLoadingConvs / isLoadingMsgs | Spinner flags for left and right panels |
| isSending | Disables 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.
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
| Condition | Display Format |
|---|---|
| Today | h:mm AM/PM (e.g. 9:45 AM) |
| Yesterday | Text 'Yesterday' |
| Older | DD 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.
| Step | What It Does |
|---|---|
| 1 — Aggregate | GROUP BY CustomerPhone__c → max(CreatedDate) + count. Limit 50 by recency. |
| 2 — Latest message per phone | Query all messages DESC for those phones. Map phone → most recent message for preview text and outgoing flag. |
| 3 — Name from messages | Prefer Contact__r.Name → Lead__r.Name → CustomerName__c from message records. |
| 4 — Fallback: query all CRM | For unresolved phones: query ALL Contacts and ALL non-converted Leads. Last-10-digit comparison. SOQL risk at scale. |
| 5 — Build wrappers | Assemble ConversationWrapper: phone, name, initial, lastMessage, lastDate, msgCount, outgoing, recordId, objType. |
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
| Field | Type / Description |
|---|---|
| phone | String — raw CustomerPhone__c from the database |
| name | String — resolved display name, or raw phone if unresolved |
| initial | String — first character of name, uppercased, used for the avatar circle |
| lastMessage | String — MessageContent__c of the most recent message |
| objType | String — 'Contact', 'Lead', or 'Unknown' |
| recordId | String — Salesforce Id of the linked Contact or Lead |
| outgoing | Boolean — true if the most recent message was sent by an agent |
| msgCount | Integer — total messages in this conversation |
| lastDate | Datetime — 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:
- Read the WhatsApp Phone Number ID and Bearer token from Named Credential or Custom Label.
- Build JSON:
{ messaging_product: 'whatsapp', recipient_type: 'individual', to: toPhone, type: 'text', text: { preview_url: false, body: messageContent } } - Set Authorization and Content-Type: application/json headers.
- On HTTP 200 — insert WAMessage__c (Outgoing__c = true) and return the record.
- On failure — throw exception with HTTP status and response body for the caller to surface.
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
- Open the Lightning App Builder. Drag the whatsAppInbox component onto any App page. Save and Activate.
- Navigate to the page. The left panel loads all conversations showing name, last message preview, and timestamp.
- 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.
- 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.
- 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.
- 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
| Fix | Action |
|---|---|
| Fix 1 — Verify Token | Move 'mysecret2024' from WhatsAppWebhook.doGet() to Custom Label WA_VERIFY_TOKEN. Update code to read Label.WA_VERIFY_TOKEN. |
| Fix 2 — Bearer Token | Store the Meta Bearer token in a Named Credential instead of a Custom Label. Update WhatsAppUtils to read from the Named Credential. |
| Fix 3 — SOQL Limit | Add 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 Logs | Wrap 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 Element | Visual Description |
|---|---|
| Left top bar | Dark green (#075E54) bar with 'WhatsApp' title in white and a refresh icon on the right |
| Search bar | Light grey strip below the top bar with a white pill-shaped input field |
| Conversation row | Each 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 screen | Shown before any conversation is selected — WhatsApp icon, 'WhatsApp Inbox' heading, 'Select a conversation' sub-text |
| Chat header | Dark green bar with circular avatar, contact name, phone number, and a 'View Contact ↗' pill link |
| Date separator | Centred pill label (Today / Yesterday / DD Mon YYYY) between message groups |
| Outgoing bubble | Light green (#D9FDD3), right-aligned, right-tail, double-tick icon bottom-right |
| Incoming bubble | White, left-aligned, left-tail, customer name in teal above the message text |
| Reply footer | Light 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
| Component | Type | Called By | Key Method / Note |
|---|---|---|---|
| WhatsAppWebhook | Apex REST | Meta Platform | doPost() — receive & validate inbound message |
| WhatsAppUtils | Apex Utility | Webhook + both services | sendTextMessage() / findLeadOrContact() |
| WAMessage__c | Custom Object | All Apex classes | Upsert on MessageID__c (External Id) |
| WA_Message_Event__e | Platform Event | Webhook on every insert | Triggers real-time LWC update via empApi |
| WhatsAppChat | LWC | Lead / Contact page | Incremental append via getSingleMessage() |
| WhatsAppLWCService | Apex Service | WhatsAppChat LWC | listAllMessages() / sendTextMessage() |
| WhatsAppInbox | LWC | Any App Builder page | Full reload via _loadMessages() on event |
| WhatsAppInboxService | Apex Service | WhatsAppInbox LWC | getAllConversations() / getMessagesByPhone() |
