Skip to main content
Skip to article

#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:

  1. Capture WhatsApp lead: Someone messages your WhatsApp Business number or mentions buying intent in a monitored group
  2. MoltFlow sends webhook: Event delivered to your endpoint (we'll use Python FastAPI, but any HTTP server works)
  3. Middleware transforms data: Extract contact info, lead source, message content, and format for CRM
  4. CRM receives lead: POST to CRM API creates/updates contact, adds note with WhatsApp message
  5. 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 received
  • message.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:

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:

json
{
  "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:

python
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

python
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.

python
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:

python
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:

python
@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:

python
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 True

Common edge cases:

  1. Duplicate contacts: Check CRM for existing record by phone before creating
  2. Missing required fields: CRM requires email but WhatsApp only has phone—use placeholder email or custom field
  3. Rate limits: Queue events in Redis/database, process in batches
  4. Partial failures: CRM accepted contact but note creation failed—log and retry just the note
  5. 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:

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

$ curl https://molt.waiflow.app/pricing

bash-5.2$ echo "End of post."_