WhatsApp lead generation bot architecture diagram — conversation state machine with GPT-4o qualification, CRM write, and human handover on dark background
What you'll build: Inbound WhatsApp lead gen bot · Conversation state machine (Redis) · GPT-4o qualification scoring · CRM API write · Slack handover alert · n8n no-code alternative · Full Node.js code throughout
What you'll build in this tutorial
Inbound WhatsApp bot that qualifies leads through conversation
Redis-backed state machine tracking each contact's stage
GPT-4o scoring leads 1–10 with JSON output
HubSpot/Pipedrive CRM write on qualification
Slack alert for human handover on high-score leads
n8n no-code alternative for the whole flow

Inbound-first: why this architecture beats outbound bots

Every no-code WhatsApp chatbot tutorial defaults to outbound — templates, BotPress, Make.com, sending messages to lists. That is the hard path. Meta requires template approval (1–5 business days), charges per outbound message, and bans numbers that send spam-flagged content. Most tutorials gloss over this and then you discover it at 11pm when your number gets restricted.

The better architecture is inbound-first:

  • Customer sends you a message first (from a Click-to-WhatsApp ad, a WhatsApp link, or a QR code)
  • That opens a free 24-hour service window — all your replies are free
  • No template approval needed for any reply within that window
  • Your webhook fires, your bot takes over, leads qualify themselves

This is not a passive strategy. You drive inbound traffic with paid ads — Click-to-WhatsApp on Facebook and Instagram sends people directly into a WhatsApp conversation with your number. Meta gives you a free 72-hour window for those conversations (not just 24). You pay for the ad click. The qualification conversation is free.

Architecture overview

Five components. All of them are replaceable — swap HubSpot for Salesforce, GPT-4o for Claude 3, Redis for DynamoDB. The architecture is the constant; the vendors are optional.

Architecture
pipeline.txt
Customer sends WhatsApp message ↓ [WhatsApp Cloud API] — fires HTTP POST webhook ↓ [SocialHook] — verifies HMAC, normalizes payload, <50ms delivery ↓ [Your Webhook Server — Node.js / Express] 1. Load conversation state from Redis (by phone number) 2. Advance state machine: ask next qualifying question 3. OR: call GPT-4o for final scoring 4. Reply via Cloud API Graph endpoint 5. Save updated state to Redis ↓ [Redis] — conversation state store (TTL: 48h per contact) ↓ On QUALIFIED (score ≥ 7): → POST to HubSpot / Pipedrive CRM API → POST to Slack webhook (sales team alert) → Reply to customer: "A specialist will reach you in 2h" On DISQUALIFIED (score < 4): → Log to database → Send helpful resource link → Close conversation

Step 1: Get the webhook layer running

Before writing bot logic, you need inbound WhatsApp messages arriving at your server as clean JSON. The fastest path: connect your WhatsApp number to SocialHook, paste your server URL as the destination, and every inbound message arrives as a normalized payload in under 50ms.

If you want direct Cloud API webhooks instead, follow the full setup guide. For this tutorial, we'll use SocialHook's normalized format which removes the need for HMAC verification code and nested payload extraction in your bot handler.

The payload your bot handler receives for every inbound customer message:

JSON
inbound-event.json (SocialHook normalized)
{ "platform": "whatsapp", "event": "message.received", "timestamp": 1747231892, "from": "+1 555 000 1234", // E.164 — the state machine key "conversation_id": "conv_8j3k...", "message": { "type": "text", "body": "Hi, I want to know more about your product", "id": "wamid.HBgL..." }, "signature_verified": true }

Step 2: Design the conversation state machine

The state machine is the core of the bot. Each WhatsApp message is a stateless HTTP event — your server has no memory of previous messages unless you store it. Redis gives you a fast, TTL-based key-value store per phone number. The state object tracks both what step the conversation is at and what data has been collected so far.

Here is the full qualification flow with 7 states:

GREETING
Hey! 👋 Thanks for reaching out to [Company]. I'm here to help find the right solution for you. What's your name?
→ COLLECT_NAME
COLLECT_NAME
Nice to meet you, [name]! What company are you from?
→ COLLECT_COMPANY
COLLECT_COMPANY
Great. What are you trying to solve — what's the main pain point bringing you here today?
→ COLLECT_USE_CASE
COLLECT_USE_CASE
Got it. Roughly how many messages or conversations do you handle per month?
→ COLLECT_VOLUME
COLLECT_VOLUME
And what's your approximate monthly budget for this kind of tool?
→ COLLECT_BUDGET
COLLECT_BUDGET
Thanks — give me just a moment while I put together the right information for you. ⏳
→ QUALIFYING (GPT-4o)
QUALIFIED (score ≥ 7)
You're a great fit for what we offer. A specialist will reach out within 2 hours. Can I confirm your email?
→ CRM write + Slack
DISQUALIFIED (score < 4)
Thanks [name]! Based on your needs, here's a resource that might help: [link]. Feel free to reach out when the timing is right.
→ Log + close

What the conversation looks like from the customer's perspective:

YourCompany Bot
● online
State: GREETING
Hey! 👋 Thanks for reaching out. I'm here to help find the right solution for you. What's your name?
Hi, I'm Sarah
State: COLLECT_COMPANY
Nice to meet you, Sarah! What company are you from?
Acme Corp, we're an e-commerce brand doing about 50k orders a month
State: COLLECT_USE_CASE
Great. What are you trying to solve — what's the main pain point bringing you here today?
We're drowning in WhatsApp support tickets. We need to automate order status and returns
State: QUALIFYING → GPT-4o scoring
Thanks — give me just a moment while I put together the right information for you. ⏳
State: QUALIFIED (score: 9/10) → CRM write + Slack alert
You're a great fit for what we offer, Sarah. A specialist will reach out within 2 hours to discuss your specific setup. Can I confirm your email for the follow-up?

Step 3: Build the state machine in Node.js

The webhook handler loads state from Redis, runs the state machine logic, replies via Cloud API, and saves the new state. This is the complete production-grade implementation:

Node.js + Express + Redis
leadBot.js
const express = require('express'); const redis = require('redis'); const { sendWhatsApp } = require('./whatsapp'); // see below const { scoreLead } = require('./scorer'); // GPT-4o scorer const { writeCRM, notifySlack } = require('./integrations'); const app = express(); const store = redis.createClient({ url: process.env.REDIS_URL }); await store.connect(); app.use(express.json()); // SocialHook posts normalized events here app.post('/webhook', async (req, res) => { res.sendStatus(200); // acknowledge immediately const event = req.body; if (event.event !== 'message.received') return; if (event.message.type !== 'text') return; const phone = event.from; const text = event.message.body.trim(); // Load state — default to GREETING if first contact const raw = await store.get(`lead:${phone}`); const state = raw ? JSON.parse(raw) : { stage: 'GREETING', data: {} }; const next = await advance(phone, text, state); // Save updated state with 48h TTL await store.set( `lead:${phone}`, JSON.stringify(next), { EX: 172800 } // 48 hours ); }); async function advance(phone, text, state) { switch (state.stage) { case 'GREETING': await sendWhatsApp(phone, "Hey! 👋 Thanks for reaching out. What's your name?" ); return { stage: 'COLLECT_NAME', data: {} }; case 'COLLECT_NAME': await sendWhatsApp(phone, `Nice to meet you, ${text}! What company are you from?` ); return { stage: 'COLLECT_COMPANY', data: { name: text } }; case 'COLLECT_COMPANY': await sendWhatsApp(phone, "What's the main pain point bringing you here today?" ); return { stage: 'COLLECT_USE_CASE', data: { ...state.data, company: text } }; case 'COLLECT_USE_CASE': await sendWhatsApp(phone, "How many conversations do you handle per month roughly?" ); return { stage: 'COLLECT_VOLUME', data: { ...state.data, useCase: text } }; case 'COLLECT_VOLUME': await sendWhatsApp(phone, "And your approximate monthly budget for this?" ); return { stage: 'COLLECT_BUDGET', data: { ...state.data, volume: text } }; case 'COLLECT_BUDGET': { await sendWhatsApp(phone, "Thanks — just a moment while I put together the right info for you. ⏳" ); const leadData = { ...state.data, budget: text }; // Score async — don't block on this, caller handles state qualifyLead(phone, leadData); // fire-and-forget with its own error handling return { stage: 'QUALIFYING', data: leadData }; } case 'COLLECT_EMAIL': { // Final step after QUALIFIED — collect email and close const finalData = { ...state.data, email: text }; await writeCRM(phone, finalData); await sendWhatsApp(phone, `Perfect, ${finalData.name}! You're all set. Talk soon. 🚀` ); return { stage: 'DONE', data: finalData }; } default: return state; // QUALIFYING / DONE / DISQUALIFIED — ignore further messages } } app.listen(3000);

Step 4: GPT-4o lead qualification scoring

Once all qualifying data is collected, you send it to GPT-4o with a structured prompt. The model returns a JSON score object — no regex parsing, no guessing. The key is instructing the model to return only JSON and nothing else.

7 – 10
Hot lead
Immediate human handover. Slack alert to sales. CRM deal created with high priority. Customer told specialist contacts in 2h.
4 – 6
Warm lead
CRM contact created with medium priority. Automated email sequence triggered. Customer sent product resources and case study.
1 – 3
Not a fit
Logged but no CRM deal. Customer sent helpful resource. Conversation closed with graceful exit message.
Node.js + OpenAI
scorer.js
const OpenAI = require('openai'); const oai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); async function scoreLead(leadData) { const prompt = `You are a B2B lead qualification expert. Score this lead from 1 to 10 based on fit and intent. Return ONLY a JSON object — no preamble, no markdown. Lead data: - Name: ${leadData.name} - Company: ${leadData.company} - Use case: ${leadData.useCase} - Volume: ${leadData.volume} conversations/month - Budget: ${leadData.budget}/month Our product: WhatsApp webhook infrastructure for developers. Ideal customer: technical team, 500+ convos/month, budget $50–$500. Return this exact JSON shape: { "score": <1-10 integer>, "rationale": "<2 sentence explanation>", "action": "handover" | "nurture" | "disqualify", "priority": "high" | "medium" | "low" }`; const response = await oai.chat.completions.create({ model: 'gpt-4o', max_tokens: 200, temperature: 0.2, // low temp = consistent scoring messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' } // force JSON output }); return JSON.parse(response.choices[0].message.content); } // Called after COLLECT_BUDGET — full async flow async function qualifyLead(phone, leadData) { try { const result = await scoreLead(leadData); if (result.action === 'handover') { await sendWhatsApp(phone, `You're a great fit, ${leadData.name}! A specialist reaches out in 2h. What's your email?` ); await store.set(`lead:${phone}`, JSON.stringify({ stage: 'COLLECT_EMAIL', data: { ...leadData, score: result.score } }), { EX: 172800 }); await notifySlack(phone, leadData, result); } else if (result.action === 'nurture') { await sendWhatsApp(phone, `Thanks ${leadData.name}! Here's a case study that fits your situation: [link]` ); } else { await sendWhatsApp(phone, `Thanks for reaching out! We might not be the best fit right now, but here's a resource: [link]` ); } } catch (err) { console.error('Scorer error:', err); // Fallback: route to human anyway await sendWhatsApp(phone, "Let me connect you with our team. One moment!"); await notifySlack(phone, leadData, { score: 'ERROR', rationale: err.message }); } } module.exports = { scoreLead, qualifyLead };

Step 5: Write qualified leads to your CRM

Once scored, you write the lead to HubSpot (or Pipedrive, Salesforce — swap the endpoint). The WhatsApp phone number becomes the contact identifier. Include the conversation summary so the sales rep has full context when they call.

Node.js + HubSpot API
integrations.js — writeCRM()
async function writeCRM(phone, leadData) { const contact = { properties: { phone: phone, firstname: leadData.name, company: leadData.company, email: leadData.email ?? '', // Custom properties — create these in HubSpot first whatsapp_use_case: leadData.useCase, whatsapp_volume: leadData.volume, whatsapp_budget: leadData.budget, lead_score: leadData.score.toString(), lead_source: 'whatsapp_bot', } }; // Create or update contact (upsert by phone) await fetch('https://api.hubapi.com/crm/v3/objects/contacts', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.HUBSPOT_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify(contact), }); } // Slack notification for human handover async function notifySlack(phone, leadData, scoring) { await fetch(process.env.SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `🔥 *New WhatsApp lead — Score: ${scoring.score}/10*`, blocks: [{ type: 'section', text: { type: 'mrkdwn', text: `*${leadData.name}* from *${leadData.company}*\n📞 ${phone}\n💬 Use case: ${leadData.useCase}\n📊 Volume: ${leadData.volume}\n💰 Budget: ${leadData.budget}\n🤖 AI rationale: ${scoring.rationale}` } }] }) }); } module.exports = { writeCRM, notifySlack };

Step 6: Sending WhatsApp replies from your server

Your bot sends replies via the WhatsApp Cloud API /messages endpoint. This is the outbound side — use your permanent System User access token and your Phone Number ID.

Node.js
whatsapp.js — sendWhatsApp()
const GRAPH = 'https://graph.facebook.com/v21.0'; const PHONE_ID = process.env.WHATSAPP_PHONE_NUMBER_ID; const TOKEN = process.env.WHATSAPP_ACCESS_TOKEN; async function sendWhatsApp(to, body) { const res = await fetch(`${GRAPH}/${PHONE_ID}/messages`, { method: 'POST', headers: { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ messaging_product: 'whatsapp', to, // E.164 — e.g. '+15550001234' type: 'text', text: { body, preview_url: false }, }), }); if (!res.ok) { throw new Error(`WhatsApp send failed: ${await res.text()}`); } return await res.json(); } module.exports = { sendWhatsApp };
Phone number format: SocialHook delivers from in E.164 with the + prefix (+15550001234). The Cloud API's to field also expects E.164. Use event.from directly as your to value — no transformation required when using SocialHook's normalized format.

No-code alternative: the n8n workflow

If you want the same bot without writing Node.js, you can build it in n8n. The architecture is identical — SocialHook delivers the webhook to n8n, and n8n handles the state logic using its code nodes and Redis integration.

n8n Workflow
n8n-lead-gen-workflow.txt
n8n Workflow: WhatsApp Lead Gen Bot ────────────────────────────────────── 1. Webhook Trigger → Method: POST → Path: /whatsapp-lead → URL: paste into SocialHook destination 2. Code Node — Load State → Redis: GET lead:{{ $json.from }} → Parse JSON or default to { stage: 'GREETING', data: {} } 3. Switch Node — Branch by state.stage → GREETING → Branch A → COLLECT_NAME → Branch B → COLLECT_* → Branch C–E → COLLECT_BUDGET → Branch F (triggers scoring) → COLLECT_EMAIL → Branch G (final CRM write) 4. HTTP Request Node (per branch) → POST to graph.facebook.com/v21.0/${PHONE_ID}/messages → Body: { messaging_product, to, type: 'text', text: { body } } → Auth: Bearer token header 5. Code Node — Update State → Redis: SET lead:{{ $json.from }} {{ JSON.stringify(newState) }} EX 172800 6. On COLLECT_BUDGET branch: → OpenAI Node: Chat Completions (gpt-4o) → Message: scoring prompt with lead data → Parse JSON response: score, action 7. Switch Node — Branch by action → handover: → Slack Node + HubSpot Create Contact → nurture: → HTTP Request (send resource message) → disqualify: → HTTP Request (graceful exit message) Dependencies: Redis integration, OpenAI integration, HubSpot integration — all available natively in n8n.
SocialHook + n8n: Configure SocialHook to forward webhook events to your n8n webhook URL. SocialHook handles Meta's HMAC verification and payload normalization — n8n receives a clean JSON body with no preprocessing needed. This works whether you're self-hosting n8n or on n8n Cloud. See the SocialHook n8n integration guide for the exact HTTP destination setup.

Step 7: Drive inbound traffic to your bot

A webhook bot with no traffic is just a server burning compute. Three channels that generate inbound WhatsApp conversations with no template approval required:

Click-to-WhatsApp ads (Facebook + Instagram)

Create a lead generation ad on Facebook or Instagram with a Click-to-WhatsApp CTA. When someone clicks, WhatsApp opens with your number pre-populated. They send a message, your 72-hour free window opens, your bot fires. This is the highest-converting inbound channel — you're targeting by interest and intent, and the jump from ad to conversation is one tap. Every conversation your bot qualifies from a CTA ad is free of Meta message fees for 72 hours.

WhatsApp link / QR code

Generate a wa.me/+{your_number}?text=Hi link. Embed it on your website, email signature, LinkedIn profile, and landing pages. Anyone who clicks opens a pre-filled WhatsApp conversation to your number. A QR code version works on printed materials, events, and packaging. Zero cost, zero setup beyond generating the link.

WhatsApp Button on your website

A floating WhatsApp button replaces your live chat widget — and converts at significantly higher rates. Visitors who click go directly to WhatsApp (mobile) or scan a QR code (desktop). Your bot handles qualification before a human sees the conversation. This replaces an expensive live chat subscription with your own qualification infrastructure at $50/month total.

Common questions

How does a WhatsApp lead generation bot work with webhooks?
When a customer messages your WhatsApp Business number, the Cloud API fires an HTTP POST to your webhook endpoint. Your server reads the message, loads the conversation state from Redis, asks the next qualifying question, sends a reply via the Cloud API, and saves the updated state. This cycle repeats for each message until the lead is scored and routed. See the full flow in the architecture section above.
Can I build this without a BSP?
Yes. Register your WhatsApp number directly through the Meta Developer Portal (free, takes 30 minutes). Use SocialHook as your webhook delivery layer — it handles HMAC verification and payload normalization. You call the Cloud API directly for outbound replies. No BSP required, no BSP platform fee. Full guide: Get API access without a BSP.
Why Redis for conversation state instead of a database?
Redis is in-memory, sub-millisecond reads, and supports TTL-based key expiry natively. Your webhook handler needs to load conversation state on every message — latency here directly impacts reply speed. A PostgreSQL or MongoDB query adds 5–50ms per message; Redis adds <1ms. The TTL feature handles abandoned conversations automatically (48h TTL means stale state is garbage-collected without any cron job). For low volume (<100 concurrent conversations), a local Map or even an SQLite database works fine.
What is the difference between inbound and outbound WhatsApp lead gen?
Inbound: customer messages you first → free 24h window (72h from Click-to-WhatsApp ads) → no template required → replies are free within quota. Outbound: you initiate contact → requires Meta-approved template → 1–5 day approval → charged per message → higher ban risk. For lead generation, inbound-first is the right architecture. Drive traffic with ads or links, let the bot handle qualification on the inbound conversation.
How much does this bot cost to run per month?
Infrastructure breakdown: SocialHook (webhook layer) = $50/month flat. Redis (Upstash free tier or $7/month) = ~$7. OpenAI GPT-4o (200 tokens per scoring call × your lead volume at ~$0.005 per scoring call) = ~$5–50 depending on volume. HubSpot (free CRM tier) = $0. Cloud API inbound = $0 (within free quota). Total for a 500-lead/month operation: ~$60–100/month. No per-lead fee, no BSP subscription, no contacts-based pricing.
How do I handle non-text messages (images, voice notes) in the bot?
Filter them in the webhook handler by checking event.message.type !== 'text' and send a gentle prompt: "Please send your answer as text so I can help you better." For voice notes, you can optionally pipe event.message.id through the Whisper API for transcription before processing — though this adds latency and cost. For lead qualification, text-only is the right starting point. See all message types explained.

Your webhook layer is
one connection away.

Connect your WhatsApp number to SocialHook and start receiving normalized JSON events to your bot in under 5 minutes. The state machine, scoring, and CRM write are yours to build — SocialHook just makes sure every inbound message arrives clean, verified, and retried.

No credit card required · $50/month after trial · Cancel anytime