#Automated WhatsApp Lead Scoring with MoltFlow
The Problem with Treating All Leads the Same
You're getting WhatsApp leads. Someone asks about pricing. Another person sends "info?". A third prospect writes three paragraphs explaining their exact requirements and budget. If you treat these identically—same response time, same sales rep assignment, same follow-up cadence—you're leaving money on the table.
The three-paragraph prospect? That's a hot lead. They're ready to buy, they're qualified, and they need immediate attention from your best closer. The "info?" message? That's tire-kicking. Maybe they become a customer eventually, but not today, and definitely not worth your top rep's time.
Lead scoring solves this. It's a system that automatically evaluates every WhatsApp interaction, assigns a numeric score based on buying signals, and triggers different actions based on score tiers. Hot leads (80-100) get instant human attention. Warm leads (50-79) enter automated nurture sequences. Cold leads (below 50) get basic auto-replies and remain in the system for long-term cultivation.
This isn't theory—it's how high-performing sales teams operate. They don't waste senior reps on unqualified prospects. They don't let hot leads wait hours for a response because no one realized they were hot. They let data and automation route leads intelligently.
What Is Lead Scoring?
Lead scoring assigns points to prospects based on two signal types: explicit (things they tell you directly) and implicit (behaviors they exhibit). The combined score indicates buying intent and qualification level.
Explicit signals:
- Budget mentioned
- Timeline stated ("need this by next week")
- Specific requirements listed ("3-bedroom condo in downtown")
- Decision authority indicated ("I'm the business owner")
- Pain points articulated
Implicit signals:
- Response speed (fast replies = engaged)
- Question depth (detailed questions = serious research)
- Message frequency (multiple messages = high interest)
- Time of day (messaging outside business hours = urgent need)
- Attachment sharing (sending documents = moving toward deal)
The magic happens when you combine signals. Someone who mentions a $50k budget (+20 points) and asks detailed technical questions (+15 points) and responds within 2 minutes to your messages (+10 points) is clearly more qualified than someone who just said "interested" and then ghosted.
The Scoring Model: Explicit + Implicit Signals
Here's a production-ready scoring framework. You'll customize the point values based on your industry, but the structure holds across use cases.
Explicit Signals Point System
| Signal Type | Examples | Points |
|---|---|---|
| Budget mentioned | "$50k budget", "500k-1M range" | +25 |
| Timeline stated | "need by Friday", "ASAP", "Q1 deadline" | +20 |
| Specific requirements | "3BR, 2BA, parking", "API integration required" | +15 |
| Decision maker | "I'm the owner", "I approve budgets" | +15 |
| Pain point | "current solution failing", "losing customers" | +15 |
| Contact info shared | Provided email, website, company name | +10 |
| Competitor mentioned | "currently using X", "evaluating Y" | +10 |
| Location specified | "in Brickell", "downtown Miami office" | +5 |
Implicit Signals Point System
| Behavior | Measurement | Points |
|---|---|---|
| Fast responses | Reply within less than5 min | +10 |
| Message depth | 50+ words per message | +10 |
| Multiple messages | 3+ messages in conversation | +10 |
| Questions asked | Asking detailed questions | +8 |
| Shares media | Sends images, docs, voice notes | +8 |
| Off-hours engagement | Messages sent 6pm-9am | +5 |
| Weekend contact | Saturday/Sunday messages | +5 |
| Follows up | Re-engages after 24+ hours | +5 |
Score Tiers and Actions
| Tier | Score Range | Label | Action |
|---|---|---|---|
| Hot | 80-100 | Ready to buy | Notify sales immediately, assign top rep, human response less than5 min |
| Warm | 50-79 | Qualified, nurture | Automated nurture sequence, assign SDR, respond less than30 min |
| Cold | 20-49 | Unqualified, long-term | Auto-reply with resources, add to newsletter, check in monthly |
| Noise | 0-19 | Not a lead | Basic auto-reply, no sales assignment |
Important: These aren't fixed forever. Leads can move between tiers as they engage more. Someone who starts cold but then mentions budget and asks detailed questions gets re-scored and promoted to warm or hot.
Step 1: Collect Signals via Webhooks
Start by capturing all WhatsApp interactions. You need historical context—not just the latest message, but the entire conversation history—to score accurately.
Set up a database to track per-contact signal accumulation:
from sqlalchemy import Column, String, Integer, Float, DateTime, JSON, create_engine
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class LeadProfile(Base):
__tablename__ = "lead_profiles"
id = Column(Integer, primary_key=True)
phone = Column(String, unique=True, index=True)
name = Column(String)
# Scoring components
explicit_score = Column(Integer, default=0)
implicit_score = Column(Integer, default=0)
total_score = Column(Integer, default=0)
# Signal tracking
signals_detected = Column(JSON, default=list) # List of detected signals with timestamps
message_count = Column(Integer, default=0)
first_contact = Column(DateTime, default=datetime.utcnow)
last_contact = Column(DateTime, default=datetime.utcnow)
# Behavioral metrics
avg_response_time = Column(Float, nullable=True) # Seconds
avg_message_length = Column(Float, nullable=True) # Character count
# Current state
tier = Column(String, default="cold") # hot/warm/cold/noise
assigned_to = Column(String, nullable=True) # Sales rep
# Metadata
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)When MoltFlow sends a webhook, update the profile:
from fastapi import FastAPI, Request
from sqlalchemy.orm import sessionmaker
from datetime import datetime
app = FastAPI()
Session = sessionmaker(bind=engine)
@app.post("/webhooks/moltflow")
async def handle_webhook(request: Request):
data = await request.json()
if data["event"] == "message.incoming":
payload = data["payload"]
phone = payload["from"].replace("@c.us", "")
message = payload["body"]
# Get or create lead profile
db = Session()
profile = db.query(LeadProfile).filter_by(phone=phone).first()
if not profile:
profile = LeadProfile(
phone=phone,
name=payload["fromName"]
)
db.add(profile)
# Update behavioral metrics
profile.message_count += 1
profile.last_contact = datetime.utcnow()
# Calculate response time if this is a reply
# (implementation depends on tracking your outbound messages)
# Update message length average
message_length = len(message)
if profile.avg_message_length:
profile.avg_message_length = (profile.avg_message_length + message_length) / 2
else:
profile.avg_message_length = message_length
# Detect and score signals
new_signals = detect_signals(message, profile)
profile.signals_detected.extend(new_signals)
# Recalculate scores
explicit, implicit = calculate_scores(profile, message)
profile.explicit_score = explicit
profile.implicit_score = implicit
profile.total_score = explicit + implicit
# Update tier
old_tier = profile.tier
profile.tier = determine_tier(profile.total_score)
db.commit()
# If tier changed (e.g., cold → warm), trigger action
if old_tier != profile.tier:
await handle_tier_change(profile, old_tier)
db.close()
return {"status": "ok"}Step 2: Calculate Scores
The scoring function evaluates both the current message and historical behavior:
import re
from typing import Tuple, List, Dict
def detect_signals(message: str, profile: LeadProfile) -> List[Dict]:
"""
Detect explicit signals in message.
Returns list of signals with type and timestamp for tracking.
"""
signals = []
message_lower = message.lower()
# Budget detection
budget_patterns = [
r'\$\d+[k]?[-–]?\$?\d*[k]?', # $50k, $50k-100k
r'\d+k?\s*budget',
r'budget.*\$?\d+',
]
for pattern in budget_patterns:
if re.search(pattern, message_lower):
signals.append({
"type": "budget_mentioned",
"points": 25,
"timestamp": datetime.utcnow().isoformat(),
"evidence": re.search(pattern, message_lower).group(0)
})
break
# Timeline detection
timeline_keywords = ["asap", "urgent", "by friday", "this week", "next month", "deadline", "need soon"]
for keyword in timeline_keywords:
if keyword in message_lower:
signals.append({
"type": "timeline_stated",
"points": 20,
"timestamp": datetime.utcnow().isoformat(),
"evidence": keyword
})
break
# Specific requirements (bedroom count, tech specs, etc.)
if re.search(r'\d+\s*(bed|br|bedroom|bath|ba)', message_lower):
signals.append({
"type": "specific_requirements",
"points": 15,
"timestamp": datetime.utcnow().isoformat(),
"evidence": "property specs mentioned"
})
# Decision maker language
decision_maker_phrases = ["i'm the owner", "i approve", "my company", "our team", "we're looking"]
if any(phrase in message_lower for phrase in decision_maker_phrases):
signals.append({
"type": "decision_maker",
"points": 15,
"timestamp": datetime.utcnow().isoformat()
})
# Pain points
pain_keywords = ["struggling with", "problem", "issue", "failing", "not working", "frustrated"]
if any(keyword in message_lower for keyword in pain_keywords):
signals.append({
"type": "pain_point",
"points": 15,
"timestamp": datetime.utcnow().isoformat()
})
# Contact info
if "@" in message and "." in message: # Simple email detection
signals.append({
"type": "contact_shared",
"points": 10,
"timestamp": datetime.utcnow().isoformat()
})
return signals
def calculate_scores(profile: LeadProfile, current_message: str) -> Tuple[int, int]:
"""
Calculate explicit and implicit scores.
Returns (explicit_score, implicit_score)
"""
# Explicit score: sum of all detected signal points
explicit_score = 0
for signal in profile.signals_detected:
explicit_score += signal["points"]
# Cap explicit at 100
explicit_score = min(explicit_score, 100)
# Implicit score: behavioral patterns
implicit_score = 0
# Message count (engagement)
if profile.message_count >= 5:
implicit_score += 10
elif profile.message_count >= 3:
implicit_score += 5
# Average message length (depth)
if profile.avg_message_length and profile.avg_message_length >= 100:
implicit_score += 10
elif profile.avg_message_length and profile.avg_message_length >= 50:
implicit_score += 5
# Fast response time (if tracked)
if profile.avg_response_time and profile.avg_response_time < 300: # <5 minutes
implicit_score += 10
# Question asking (current message analysis)
if "?" in current_message:
implicit_score += 8
# Current message length
if len(current_message) >= 100:
implicit_score += 5
# Time-based signals (check last_contact timestamp)
hour = profile.last_contact.hour
if hour < 9 or hour > 18: # Off-hours
implicit_score += 5
if profile.last_contact.weekday() >= 5: # Weekend
implicit_score += 5
# Cap implicit at 50 (implicit is supporting evidence, not primary)
implicit_score = min(implicit_score, 50)
return explicit_score, implicit_score
def determine_tier(total_score: int) -> str:
"""Map score to tier"""
if total_score >= 80:
return "hot"
elif total_score >= 50:
return "warm"
elif total_score >= 20:
return "cold"
else:
return "noise"What this does: Every incoming message updates the lead's profile, accumulates signal history, recalculates scores based on both new and historical data, and assigns a tier. The scoring is dynamic—profiles evolve as leads engage more.
Step 3: Automate Actions by Score Tier
Different score tiers trigger different workflows. This is where scoring translates into revenue.
import httpx
import os
async def handle_tier_change(profile: LeadProfile, old_tier: str):
"""
Execute tier-specific actions when a lead's score changes.
"""
new_tier = profile.tier
print(f"Lead {profile.name} moved from {old_tier} → {new_tier} (score: {profile.total_score})")
if new_tier == "hot":
await handle_hot_lead(profile)
elif new_tier == "warm" and old_tier in ["cold", "noise"]:
await handle_warm_lead(profile)
elif new_tier == "cold" and old_tier == "noise":
await handle_cold_lead(profile)
async def handle_hot_lead(profile: LeadProfile):
"""HOT lead actions: immediate sales notification + priority response"""
# 1. Notify sales team (Slack, email, SMS)
await notify_sales_team(
f"🔥 HOT LEAD: {profile.name} (Score: {profile.total_score})\n"
f"Phone: {profile.phone}\n"
f"Signals: {', '.join([s['type'] for s in profile.signals_detected[-3:]])}\n"
f"Action: Respond within 5 minutes!"
)
# 2. Assign to top sales rep
top_rep = "[email protected]" # Your logic for assignment
profile.assigned_to = top_rep
# 3. Send immediate personalized DM
await send_whatsapp_message(
phone=profile.phone,
message=f"Hi {profile.name}! Thanks for reaching out. I'm Sarah, and I'd love to help with your search. Are you available for a quick call in the next 30 minutes? I have some options that match exactly what you described."
)
async def handle_warm_lead(profile: LeadProfile):
"""WARM lead actions: automated nurture sequence"""
# 1. Assign to SDR (sales development rep)
profile.assigned_to = "[email protected]"
# 2. Send helpful resources
await send_whatsapp_message(
phone=profile.phone,
message=f"Hi {profile.name}! I saw your message. I put together a quick guide that answers the most common questions we get—sending it over now. Let me know if you want to chat!"
)
# 3. Schedule follow-up in 24 hours
# (use Celery, APScheduler, or your task queue)
schedule_task("send_followup", delay_hours=24, lead_id=profile.id)
async def handle_cold_lead(profile: LeadProfile):
"""COLD lead actions: basic auto-reply, long-term nurture"""
await send_whatsapp_message(
phone=profile.phone,
message="Thanks for your message! Here's our website with pricing and details: https://company.com. Feel free to reach out anytime with questions!"
)
# Add to monthly newsletter
# (CRM integration or email service)
async def send_whatsapp_message(phone: str, message: str):
"""Send message via MoltFlow API"""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://apiv2.waiflow.app/api/v2/sessions/my-session/messages",
headers={
"Authorization": f"Bearer {os.getenv('MOLTFLOW_API_KEY')}",
"Content-Type": "application/json"
},
json={
"chatId": f"{phone}@c.us",
"text": message
}
)
return response.status_code == 200
async def notify_sales_team(message: str):
"""Send notification via Slack or email"""
# Slack webhook example
async with httpx.AsyncClient() as client:
await client.post(
os.getenv("SLACK_WEBHOOK_URL"),
json={"text": message}
)Key insight: Automation doesn't replace sales—it enables sales to focus on high-value activities. Your top closer shouldn't be answering "info?" messages. They should be on calls with 85-score leads who already mentioned budget and timeline.
Step 4: Track and Optimize
Lead scoring is never "done." You need to measure which signals actually predict conversion, then adjust point values accordingly.
Build a conversion tracking table:
class Conversion(Base):
__tablename__ = "conversions"
id = Column(Integer, primary_key=True)
phone = Column(String, index=True)
score_at_conversion = Column(Integer)
tier_at_conversion = Column(String)
signals_at_conversion = Column(JSON)
converted_at = Column(DateTime, default=datetime.utcnow)
revenue = Column(Float, nullable=True)When a lead converts (signs contract, makes purchase), record their score:
def record_conversion(phone: str, revenue: float):
"""Log conversion for scoring optimization"""
db = Session()
profile = db.query(LeadProfile).filter_by(phone=phone).first()
conversion = Conversion(
phone=phone,
score_at_conversion=profile.total_score,
tier_at_conversion=profile.tier,
signals_at_conversion=profile.signals_detected,
revenue=revenue
)
db.add(conversion)
db.commit()
db.close()Analyze conversion data monthly:
-- Which signals appear most often in conversions?
SELECT signal->>'type' as signal_type, COUNT(*) as occurrences
FROM conversions, jsonb_array_elements(signals_at_conversion) as signal
GROUP BY signal_type
ORDER BY occurrences DESC;
-- Average score of converted vs non-converted leads
SELECT
AVG(score_at_conversion) as avg_converted_score,
(SELECT AVG(total_score) FROM lead_profiles WHERE tier != 'noise') as avg_all_leads
FROM conversions;Optimization example: If you find that "timeline_stated" appears in 80% of conversions but "location_specified" only in 20%, increase timeline points to 25 and decrease location to 3.
Real-World Impact
A real estate agency implemented this exact scoring system. Before automation, every lead got the same treatment—agents spent equal time on tire-kickers and serious buyers. After scoring:
- Hot leads (15% of volume) got assigned to senior agents within 5 minutes → 42% close rate
- Warm leads (35% of volume) went through automated nurture → 18% eventual conversion
- Cold leads (50% of volume) got auto-replies → 3% long-term conversion
Revenue impact: By focusing senior agents on just the 15% hot tier, the agency increased closed deals by 60% with the same team size. The automated nurture recovered an additional 25 deals monthly that would've been ignored under manual processes.
A SaaS company scored leads based on technical questions asked, integrations mentioned, and team size indicated. Leads mentioning "API", "Salesforce integration", and "50+ employees" scored 80+. Those leads got immediate founder calls. Result: enterprise deal cycle dropped from 60 days to 28 days because hot leads weren't stuck in generic nurture sequences.
Common Pitfalls to Avoid
Pitfall 1: Static scoring. If you set point values once and never adjust, you'll miss changing buyer behavior. Review quarterly, adjust based on conversion data.
Pitfall 2: Over-automation of hot leads. Don't send automated messages to 90-score leads. They need humans. Automation is for warm/cold tiers.
Pitfall 3: Ignoring tier movement. A lead who starts cold can become hot. If you filed them as "unqualified" and stopped paying attention, you miss the upgrade. Always re-score on new activity.
Pitfall 4: Too many tiers. Don't create 10 different tiers with subtle differences. Three tiers (hot/warm/cold) are enough. Complexity kills execution.
What's Next: Advanced Scoring
Once basic scoring works, enhance with:
- Machine learning models: Train on historical conversion data to predict scores more accurately
- Multi-channel scoring: Combine WhatsApp behavior with website visits, email opens, and LinkedIn engagement
- Predictive churn scoring: Identify when hot leads are cooling off (decreased engagement) and trigger re-engagement
- A/B test automation: Run experiments on different tier thresholds and measure impact on conversion
Continue learning:
- WhatsApp Lead Capture & CRM Integration — Push scored leads to HubSpot, Salesforce, or custom CRMs
- WhatsApp Group Lead Generation — Apply scoring to group-sourced leads
- AI Lead Qualification — Use conversational AI for dynamic scoring
- WhatsApp Conversion Metrics — Track ROI and optimize scoring models
Ready to implement lead scoring? Follow our step-by-step guide: Keyword Rules for Lead Capture
Start Scoring Today
You have the framework. Next steps:
- Set up the database schema (lead_profiles table)
- Wire MoltFlow webhooks to your scoring endpoint
- Define initial point values for your industry
- Configure tier-specific actions (notifications, assignments, auto-replies)
- Track conversions for optimization
Sign up for MoltFlow and start building your scoring system. Stop wasting time on low-intent prospects. Let data tell you who's ready to buy.
> Try MoltFlow Free — 100 messages/month