Tools: Building a Lead Capture Bot with WhatsApp and n8n (Open-Source Step-by-Step)

Tools: Building a Lead Capture Bot with WhatsApp and n8n (Open-Source Step-by-Step)

Architecture Overview

Prerequisites

Step 1: Deploy WAHA with Docker

Step 2: Configure the Webhook

Step 3: Build the n8n Workflow

Node 1: Webhook (Trigger)

Node 2: Filter (IF Node)

Node 3: Extract Lead Data (Set Node)

Node 4: Save to Database

Node 5: Auto-Reply (HTTP Request)

The Complete Workflow

Handling Edge Cases

Security Considerations

Why Self-Host?

Next Steps WhatsApp has over 2 billion users worldwide. If your business communicates with leads over WhatsApp, you're probably doing it manually — reading messages, copying contact details into a spreadsheet, and hoping nothing falls through the cracks. In this tutorial, I'll show you how to build a fully open-source, self-hosted lead capture bot that automatically collects lead information from WhatsApp conversations and stores it in your CRM or database. No paid APIs, no vendor lock-in. Disclaimer: WAHA is an unofficial, third-party tool that interacts with WhatsApp Web. It is not endorsed by, affiliated with, or supported by Meta or WhatsApp. Using unofficial APIs may violate WhatsApp's Terms of Service. Use at your own risk and evaluate your compliance requirements before deploying in production. Here's the full data flow: Each component is self-hosted on your own infrastructure. You own the data end-to-end. WAHA runs as a Docker container. Start it with: Once running, open http://your-server:3000/dashboard to scan the QR code with your WhatsApp and authenticate the session. Reminder: WAHA is unofficial software. It reverse-engineers WhatsApp Web. This is not a Meta-approved integration method. WAHA can send incoming messages to any webhook URL. We'll point it to our n8n instance. Use the WAHA API to set the webhook: When someone sends a message, WAHA will POST a payload like this to your n8n webhook: The from field contains the phone number. The body field contains the message text. That's all we need to start capturing leads. In n8n, create a new workflow with the following nodes: This is the entry point. Every incoming WhatsApp message hits this node. We only want to capture new leads, not every message. Add a filter: This prevents the bot from treating every casual message as a lead. Adjust the keywords to fit your business. Parse the payload into clean lead fields: Connect to your database or CRM. Here are common options: PostgreSQL / Supabase: Google Sheets: Use n8n's built-in Google Sheets node to append a row. HTTP Request (any CRM API): POST the lead data to HubSpot, Pipedrive, or any CRM with a REST API. Send an automatic acknowledgment back through WAHA: In n8n, this is an HTTP Request node: Your n8n workflow should now look like this: The DB Insert and auto-reply nodes can run in parallel — use n8n's split output to trigger both simultaneously. A few things to consider for production: Since you're self-hosting everything: Using open-source, self-hosted tools like n8n and WAHA means: The trade-off is that you're responsible for maintenance, uptime, and compliance — and you're using an unofficial API that could break if WhatsApp changes their web client. Once the basic lead capture is working, you can extend it: For a deeper dive into WhatsApp bot architecture and more advanced patterns, check out our complete WhatsApp bot guide. If you're exploring business automation more broadly — not just WhatsApp — our business automation guide covers the full landscape of what you can automate with open-source tools. I build automation systems for businesses using open-source tools like n8n, WAHA, and Supabase. If you want help setting up lead capture or any other workflow, visit achiya-automation.com to learn more. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

User sends WhatsApp message │ ▼ ┌─────────┐ │ WAHA │ (WhatsApp Web wrapper, self-hosted) └────┬────┘ │ Webhook POST ▼ ┌─────────┐ │ n8n │ (Workflow engine, self-hosted) └────┬────┘ │ Processes message, extracts lead data ▼ ┌──────────────┐ │ CRM/Database │ (Supabase, PostgreSQL, Google Sheets, etc.) └──────────────┘ User sends WhatsApp message │ ▼ ┌─────────┐ │ WAHA │ (WhatsApp Web wrapper, self-hosted) └────┬────┘ │ Webhook POST ▼ ┌─────────┐ │ n8n │ (Workflow engine, self-hosted) └────┬────┘ │ Processes message, extracts lead data ▼ ┌──────────────┐ │ CRM/Database │ (Supabase, PostgreSQL, Google Sheets, etc.) └──────────────┘ User sends WhatsApp message │ ▼ ┌─────────┐ │ WAHA │ (WhatsApp Web wrapper, self-hosted) └────┬────┘ │ Webhook POST ▼ ┌─────────┐ │ n8n │ (Workflow engine, self-hosted) └────┬────┘ │ Processes message, extracts lead data ▼ ┌──────────────┐ │ CRM/Database │ (Supabase, PostgreSQL, Google Sheets, etc.) └──────────────┘ docker run -d \ --name waha \ -p 3000:3000 \ -e WHATSAPP_DEFAULT_ENGINE=WEBJS \ -e WAHA_DASHBOARD_ENABLED=true \ devlikeapro/waha docker run -d \ --name waha \ -p 3000:3000 \ -e WHATSAPP_DEFAULT_ENGINE=WEBJS \ -e WAHA_DASHBOARD_ENABLED=true \ devlikeapro/waha docker run -d \ --name waha \ -p 3000:3000 \ -e WHATSAPP_DEFAULT_ENGINE=WEBJS \ -e WAHA_DASHBOARD_ENABLED=true \ devlikeapro/waha curl -X PUT http://localhost:3000/api/session/default \ -H "Content-Type: application/json" \ -d '{ "config": { "webhooks": [ { "url": "https://your-n8n-domain.com/webhook/whatsapp-lead", "events": ["message"] } ] } }' curl -X PUT http://localhost:3000/api/session/default \ -H "Content-Type: application/json" \ -d '{ "config": { "webhooks": [ { "url": "https://your-n8n-domain.com/webhook/whatsapp-lead", "events": ["message"] } ] } }' curl -X PUT http://localhost:3000/api/session/default \ -H "Content-Type: application/json" \ -d '{ "config": { "webhooks": [ { "url": "https://your-n8n-domain.com/webhook/whatsapp-lead", "events": ["message"] } ] } }' { "event": "message", "session": "default", "payload": { "id": "[email protected]_3EB0A608C4A3", "timestamp": 1710500000, "from": "[email protected]", "body": "Hi, I'm interested in your automation services", "hasMedia": false } } { "event": "message", "session": "default", "payload": { "id": "[email protected]_3EB0A608C4A3", "timestamp": 1710500000, "from": "[email protected]", "body": "Hi, I'm interested in your automation services", "hasMedia": false } } { "event": "message", "session": "default", "payload": { "id": "[email protected]_3EB0A608C4A3", "timestamp": 1710500000, "from": "[email protected]", "body": "Hi, I'm interested in your automation services", "hasMedia": false } } { "phone": "{{ $json.payload.from.replace('@c.us', '') }}", "message": "{{ $json.payload.body }}", "source": "whatsapp", "captured_at": "{{ $now.toISO() }}" } { "phone": "{{ $json.payload.from.replace('@c.us', '') }}", "message": "{{ $json.payload.body }}", "source": "whatsapp", "captured_at": "{{ $now.toISO() }}" } { "phone": "{{ $json.payload.from.replace('@c.us', '') }}", "message": "{{ $json.payload.body }}", "source": "whatsapp", "captured_at": "{{ $now.toISO() }}" } INSERT INTO leads (phone, message, source, captured_at) VALUES ($1, $2, $3, $4) ON CONFLICT (phone) DO UPDATE SET last_message = $2, updated_at = $4; INSERT INTO leads (phone, message, source, captured_at) VALUES ($1, $2, $3, $4) ON CONFLICT (phone) DO UPDATE SET last_message = $2, updated_at = $4; INSERT INTO leads (phone, message, source, captured_at) VALUES ($1, $2, $3, $4) ON CONFLICT (phone) DO UPDATE SET last_message = $2, updated_at = $4; curl -X POST http://localhost:3000/api/sendText \ -H "Content-Type: application/json" \ -d '{ "session": "default", "chatId": "[email protected]", "text": "Thanks for reaching out! We received your message and will get back to you shortly." }' curl -X POST http://localhost:3000/api/sendText \ -H "Content-Type: application/json" \ -d '{ "session": "default", "chatId": "[email protected]", "text": "Thanks for reaching out! We received your message and will get back to you shortly." }' curl -X POST http://localhost:3000/api/sendText \ -H "Content-Type: application/json" \ -d '{ "session": "default", "chatId": "[email protected]", "text": "Thanks for reaching out! We received your message and will get back to you shortly." }' Webhook → IF (keyword filter) → Set (extract data) → DB Insert → HTTP Request (auto-reply) Webhook → IF (keyword filter) → Set (extract data) → DB Insert → HTTP Request (auto-reply) Webhook → IF (keyword filter) → Set (extract data) → DB Insert → HTTP Request (auto-reply) - WAHA — an unofficial, open-source WhatsApp API (not affiliated with or endorsed by Meta/WhatsApp). It wraps WhatsApp Web into a REST API. - n8n — an open-source workflow automation platform you can self-host. - A VPS or local machine with Docker installed - Basic familiarity with REST APIs and JSON - A spare phone number for WhatsApp (don't use your personal number for development) - Type: Webhook - HTTP Method: POST - Path: whatsapp-lead - Condition: {{ $json.payload.body }} contains keywords like "interested", "pricing", "demo", "info" - Method: POST - URL: http://waha:3000/api/sendText - Body: JSON with session, chatId (from the incoming payload), and text - Duplicate messages: WAHA may occasionally deliver the same message twice. Use the message id field as a deduplication key in your database. - Rate limiting: WhatsApp may flag accounts that send too many messages too quickly. Keep auto-replies minimal and add delays if needed. - Session persistence: WAHA sessions can expire. Mount a Docker volume (-v waha_data:/app/store) to persist session data across container restarts. - Media messages: If hasMedia is true, you'll need to call WAHA's media download endpoint separately. For lead capture, text messages are usually sufficient. - Put WAHA behind a reverse proxy (Nginx/Traefik) with HTTPS - Use n8n's built-in authentication for the webhook (Header Auth or Basic Auth) - Never expose WAHA's API port directly to the internet without authentication - Store lead data in compliance with your local privacy regulations - No per-message fees — unlike the official WhatsApp Business API, which charges per conversation - Full data ownership — lead data never passes through a third-party SaaS - Customizable — modify the workflow to match your exact business logic - No vendor lock-in — swap any component without rebuilding everything - Add a conversational flow with multiple steps (ask for name, email, budget) - Connect to a calendar booking system for automatic demo scheduling - Build lead scoring based on message content and response patterns - Add multi-language support by detecting the message language