#Connect WhatsApp Leads to Your CRM with MoltFlow
Your CRM Doesn't Know WhatsApp Exists
Most businesses run their sales process through a CRM. Leads come in via form submissions, email, or sales calls, get scored and routed, move through pipeline stages, trigger automation. It's organized, measurable, and accountable.
But then there's WhatsApp. Conversations happen there—qualification calls, pricing discussions, objection handling, deal negotiations—and none of it touches your CRM. Your sales reports show 30 leads this month. Reality? 30 known leads. The other 15 WhatsApp conversations never got logged because manual data entry doesn't happen consistently.
This creates blind spots. You can't accurately forecast revenue when half your pipeline is invisible. You can't analyze conversion rates when your data is incomplete. You can't automate follow-ups when the CRM doesn't know a lead exists.
The fix isn't "be better about manual entry." It's connecting WhatsApp directly to your CRM so every lead, every interaction, and every deal stage change syncs automatically. This guide shows you how to build that integration using MoltFlow's webhook system.
Why Webhook-Based Integration Wins
You could manually copy-paste WhatsApp conversations into your CRM. Some teams do. It doesn't scale, and it breaks the moment someone forgets. You could use Zapier or Make.com as middleware. That works, but you're paying per-task, hitting rate limits, and debugging no-code logic when things break.
Webhook-based integration is different. MoltFlow sends you events in real-time (new message, contact update, status change). Your code receives those events, transforms them to match your CRM's format, and pushes via the CRM's API. You control the logic, handle edge cases programmatically, and pay nothing extra.
The architecture is simple: WhatsApp → MoltFlow → Your Webhook Endpoint → CRM API. Each piece has one job, and failures are isolated. If your CRM is down, your webhook can queue events and retry later. You can't do that with Zapier.
Integration Pattern Overview
Here's the flow we'll build:
- Capture WhatsApp lead: Someone messages your WhatsApp Business number or mentions buying intent in a monitored group
- MoltFlow sends webhook: Event delivered to your endpoint (we'll use Python FastAPI, but any HTTP server works)
- Middleware transforms data: Extract contact info, lead source, message content, and format for CRM
- CRM receives lead: POST to CRM API creates/updates contact, adds note with WhatsApp message
- Confirmation logged: Your webhook returns 200 OK, MoltFlow marks delivery successful
You'll end up with a system where every WhatsApp interaction automatically appears in your CRM within seconds, tagged with source, scored, and ready for sales to action.
Step 1: Set Up MoltFlow Webhooks
First, configure where MoltFlow sends events. In your MoltFlow dashboard, go to Settings → Webhooks and add your endpoint URL.
Webhook URL format: https://yourdomain.com/webhooks/moltflow
Events to subscribe to:
message.incoming- New messages receivedmessage.sent- Confirmation of sent messages (for tracking outbound in CRM)contact.update- Contact profile changes (name, profile pic)
Local development tip: Use ngrok to tunnel localhost to a public URL for testing: ngrok http 8000 gives you a temporary HTTPS endpoint MoltFlow can reach.
Here's the basic webhook receiver in Python:
from fastapi import FastAPI, Request, HTTPException
from datetime import datetime
import httpx
import os
app = FastAPI()
# MoltFlow webhook secret (get from dashboard)
WEBHOOK_SECRET = os.getenv("MOLTFLOW_WEBHOOK_SECRET")
@app.post("/webhooks/moltflow")
async def handle_moltflow_webhook(request: Request):
"""Receive events from MoltFlow and route to CRM"""
# Verify webhook signature (production requirement)
signature = request.headers.get("X-MoltFlow-Signature")
# TODO: Implement signature verification (see MoltFlow docs)
data = await request.json()
event_type = data.get("event")
print(f"Received event: {event_type}")
# Route to appropriate handler
if event_type == "message.incoming":
await handle_incoming_message(data)
elif event_type == "message.sent":
await handle_sent_message(data)
elif event_type == "contact.update":
await handle_contact_update(data)
return {"status": "received"}Security note: Always verify webhook signatures in production. This ensures events actually come from MoltFlow, not malicious actors. The signature verification code is in MoltFlow's docs.
Step 2: Extract Lead Data from Webhooks
When MoltFlow sends a message.incoming event, you get rich data about the contact and message. Here's a typical payload:
{
"event": "message.incoming",
"session": "my-business-account",
"timestamp": "2026-02-06T14:30:00Z",
"payload": {
"id": "msg_abc123",
"from": "[email protected]",
"fromName": "Jessica Martinez",
"chatId": "[email protected]",
"body": "Hi! I saw your ad about the Miami condos. Do you have anything available in the $400k-500k range?",
"timestamp": 1707234600,
"hasMedia": false
}
}Transform this into a lead object your CRM understands:
from typing import Dict, Optional
import re
def extract_lead_from_message(event_data: dict) -> Optional[Dict]:
"""
Extract lead information from incoming message.
Returns None if message doesn't qualify as a lead.
"""
payload = event_data["payload"]
# Parse phone number (remove WhatsApp suffix)
phone = payload["from"].replace("@c.us", "")
# Format phone for CRM (add country code if missing)
if not phone.startswith("+"):
phone = f"+{phone}"
# Extract name
name = payload["fromName"]
first_name, last_name = split_name(name)
# Detect lead intent (simple keyword matching)
message = payload["body"]
is_lead = detect_lead_intent(message)
if not is_lead:
return None # Don't create CRM entry for casual messages
# Build lead object
lead = {
"phone": phone,
"first_name": first_name,
"last_name": last_name,
"source": "WhatsApp",
"source_detail": f"MoltFlow - {event_data['session']}",
"initial_message": message,
"lead_score": calculate_simple_score(message),
"created_at": payload["timestamp"]
}
# Extract budget if mentioned
budget = extract_budget(message)
if budget:
lead["budget"] = budget
return lead
def split_name(full_name: str) -> tuple:
"""Split 'First Last' into components"""
parts = full_name.strip().split(" ", 1)
first_name = parts[0]
last_name = parts[1] if len(parts) > 1 else ""
return first_name, last_name
def detect_lead_intent(message: str) -> bool:
"""Check if message indicates buying intent"""
intent_keywords = [
"interested", "looking for", "do you have", "available",
"pricing", "cost", "how much", "buy", "purchase"
]
message_lower = message.lower()
return any(keyword in message_lower for keyword in intent_keywords)
def extract_budget(message: str) -> Optional[str]:
"""Extract budget mention like '$400k-500k' or '400-500 thousand'"""
patterns = [
r'\$(\d+)k[-–]?\$?(\d+)k', # $400k-$500k
r'(\d+)[-–](\d+)k budget', # 400-500k budget
r'around \$(\d+)k', # around $450k
]
for pattern in patterns:
match = re.search(pattern, message, re.IGNORECASE)
if match:
return match.group(0) # Return matched string
return None
def calculate_simple_score(message: str) -> int:
"""Basic lead scoring 0-100"""
score = 0
# High-intent words
if any(word in message.lower() for word in ["buy", "purchase", "interested"]):
score += 30
# Budget mention = qualified
if extract_budget(message):
score += 25
# Specific requirements = serious
if re.search(r'\d+\s*(bed|bath)', message.lower()):
score += 20
# Urgency
if any(word in message.lower() for word in ["asap", "soon", "urgently", "this week"]):
score += 15
# Question = engaged
if "?" in message:
score += 10
return min(score, 100)What this does: Filters out non-leads (casual "hi" messages), extracts structured data (name, phone, budget), and assigns an initial score. Your CRM gets clean, actionable data instead of raw WhatsApp payloads.
Step 3: Push to Your CRM
Now send the extracted lead to your CRM. I'll show three common integrations: HubSpot, Google Sheets, and a generic REST API pattern.
Option A: HubSpot Integration
import httpx
import os
HUBSPOT_API_KEY = os.getenv("HUBSPOT_API_KEY")
async def send_to_hubspot(lead: dict):
"""Create or update contact in HubSpot"""
# HubSpot requires specific property names
hubspot_contact = {
"properties": {
"email": lead.get("email", f"{lead['phone']}@whatsapp.placeholder"), # HubSpot requires email
"firstname": lead["first_name"],
"lastname": lead["last_name"],
"phone": lead["phone"],
"hs_lead_status": "NEW",
"lead_source": lead["source"],
"lead_source_detail": lead["source_detail"],
"initial_whatsapp_message": lead["initial_message"],
"hubspot_owner_id": "12345678" # Assign to sales rep
}
}
async with httpx.AsyncClient() as client:
# Try to create contact
response = await client.post(
"https://api.hubapi.com/crm/v3/objects/contacts",
headers={
"Authorization": f"Bearer {HUBSPOT_API_KEY}",
"Content-Type": "application/json"
},
json=hubspot_contact
)
if response.status_code == 201:
contact = response.json()
print(f"✓ Created HubSpot contact: {contact['id']}")
# Add note with WhatsApp message
await add_hubspot_note(contact["id"], lead["initial_message"])
elif response.status_code == 409:
# Contact exists, update instead
print("Contact exists, updating...")
# TODO: Search by phone, get ID, then update
else:
print(f"✗ HubSpot error: {response.status_code} - {response.text}")
async def add_hubspot_note(contact_id: str, message: str):
"""Add note to HubSpot contact with WhatsApp message"""
note = {
"properties": {
"hs_note_body": f"**WhatsApp Message:**\n\n{message}",
"hs_timestamp": int(datetime.now().timestamp() * 1000)
},
"associations": [
{
"to": {"id": contact_id},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 202}]
}
]
}
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.hubapi.com/crm/v3/objects/notes",
headers={
"Authorization": f"Bearer {HUBSPOT_API_KEY}",
"Content-Type": "application/json"
},
json=note
)
if response.status_code == 201:
print(f"✓ Added note to contact {contact_id}")Option B: Google Sheets Integration
Perfect for small teams or MVP stage. Leads appear in a spreadsheet your team already uses.
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
from datetime import datetime
async def send_to_google_sheets(lead: dict):
"""Append lead to Google Sheet"""
# Authenticate (requires service account JSON)
creds = Credentials.from_service_account_file(
"service-account.json",
scopes=["https://www.googleapis.com/auth/spreadsheets"]
)
service = build("sheets", "v4", credentials=creds)
sheet_id = "YOUR_SPREADSHEET_ID"
# Format row
row = [
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # Timestamp
lead["first_name"],
lead["last_name"],
lead["phone"],
lead["initial_message"],
lead["lead_score"],
lead.get("budget", ""),
"NEW" # Status
]
# Append to sheet
result = service.spreadsheets().values().append(
spreadsheetId=sheet_id,
range="Leads!A:H", # Your sheet name and range
valueInputOption="RAW",
body={"values": [row]}
).execute()
print(f"✓ Added to Google Sheets: {result.get('updates').get('updatedCells')} cells")Option C: Generic REST API Pattern
For Salesforce, Pipedrive, custom CRMs, or any system with an API:
async def send_to_custom_crm(lead: dict):
"""Generic REST API integration"""
# Transform to your CRM's format
crm_payload = {
# Map fields to your CRM's schema
"contact": {
"name": f"{lead['first_name']} {lead['last_name']}",
"phone": lead["phone"],
"lead_source": "WhatsApp",
"custom_fields": {
"whatsapp_message": lead["initial_message"],
"lead_score": lead["lead_score"]
}
}
}
async with httpx.AsyncClient() as client:
response = await client.post(
"https://your-crm.com/api/v1/contacts",
headers={
"Authorization": f"Token {os.getenv('CRM_API_KEY')}",
"Content-Type": "application/json"
},
json=crm_payload,
timeout=10.0
)
if response.status_code in [200, 201]:
print(f"✓ Lead synced to CRM")
return response.json()
else:
print(f"✗ CRM sync failed: {response.status_code}")
raise HTTPException(status_code=500, detail="CRM sync failed")Step 4: Handle Two-Way Sync
So far, we've pushed WhatsApp leads into your CRM. But what about the other direction? When your sales rep closes a deal in the CRM, you want that status reflected in your lead tracking, so WhatsApp follow-ups stop automatically.
Two-way sync requires your CRM to webhook back to you when records update. Most modern CRMs support this (HubSpot workflows, Salesforce Process Builder, Pipedrive webhooks).
Here's a simplified two-way sync handler:
@app.post("/webhooks/hubspot")
async def handle_hubspot_update(request: Request):
"""Receive updates from HubSpot when contact status changes"""
data = await request.json()
# HubSpot sends arrays of events
for event in data:
if event.get("subscriptionType") == "contact.propertyChange":
property_name = event["propertyName"]
contact_id = event["objectId"]
if property_name == "lifecyclestage":
# Contact became customer - stop WhatsApp automation
new_value = event["propertyValue"]
if new_value == "customer":
phone = await get_contact_phone_from_hubspot(contact_id)
await disable_whatsapp_automation(phone)
return {"status": "ok"}
async def disable_whatsapp_automation(phone: str):
"""Stop automated messages for converted customer"""
# Update your internal database
# Or call MoltFlow API to unlabel contact
print(f"✓ Disabled automation for {phone} - they're a customer now!")Why this matters: Without two-way sync, your automation keeps nurturing people who already bought. Customers get annoyed. With two-way sync, pipeline stages in your CRM control WhatsApp automation state automatically.
Edge Cases and Error Handling
Real-world integrations break. APIs go down, rate limits hit, data validation fails. Handle these gracefully:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
async def send_to_crm_with_retry(lead: dict):
"""Retry CRM sync up to 3 times with exponential backoff"""
try:
await send_to_hubspot(lead)
except httpx.HTTPError as e:
print(f"CRM sync attempt failed: {e}")
raise # Trigger retry
except Exception as e:
print(f"Non-retryable error: {e}")
# Log to error tracking (Sentry, etc.)
# Don't raise - return failure
return False
return TrueCommon edge cases:
- Duplicate contacts: Check CRM for existing record by phone before creating
- Missing required fields: CRM requires email but WhatsApp only has phone—use placeholder email or custom field
- Rate limits: Queue events in Redis/database, process in batches
- Partial failures: CRM accepted contact but note creation failed—log and retry just the note
- Webhook replay: MoltFlow retries failed deliveries—use idempotency keys to prevent duplicate CRM entries
What You've Built
At this point, you have:
- WhatsApp messages flowing into your CRM in real-time
- Automatic lead scoring and qualification
- Sales team working from complete, accurate data
- Two-way sync keeping systems in alignment
The impact? Sales knows every active conversation, marketing can measure WhatsApp as a channel, leadership sees accurate pipeline forecasts. No more blind spots.
Next Steps: Advanced CRM Workflows
Once basic sync works, level up with:
- Lead Scoring Automation — Build multi-signal scoring that updates in real-time as leads engage
- WhatsApp Group Lead Generation — Capture leads from group conversations automatically
- AI Lead Qualification — Pre-qualify leads with conversational AI before CRM entry
- Track Usage & Plan Limits — Monitor API usage and conversion metrics
Ready to implement CRM integration? Follow our step-by-step guide: REST API Quick Start
Start Syncing Today
You've got the code. Next step is deploying your webhook endpoint (Heroku, Railway, DigitalOcean—anywhere that runs Python) and configuring MoltFlow to send events to it.
Start simple: sync new WhatsApp leads to a Google Sheet. Verify the data looks right. Then upgrade to your full CRM integration with scoring, routing, and automation.
Sign up for MoltFlow to get your webhook credentials and start building. Your CRM is about to discover WhatsApp exists.
> Try MoltFlow Free — 100 messages/month