Back to blog
Chatmaid Developers··12 min read

Build a WhatsApp Chatbot for Real Estate Lead Qualification

Build a WhatsApp chatbot that qualifies real estate leads 24/7 with n8n, OpenAI and Chatmaid: the questions, system prompt, lead sheet and agent alerts.

Chatmaid Team
Build a WhatsApp Chatbot for Real Estate Lead Qualification

To build a WhatsApp chatbot that qualifies real estate leads around the clock, you connect three pieces: your agency's WhatsApp number linked to Chatmaid Developers, an n8n workflow, and an OpenAI model. Chatmaid sends each lead's message to n8n as a message.received webhook. n8n looks up the conversation so far, asks the model for the next qualifying question, and sends the reply back through Chatmaid's POST /v1/messages/send endpoint. When a lead is ready to move, the workflow sends an agent a summary on WhatsApp. This guide gives you the questions, the system prompt and every node.

Real estate leads don't wait. A buyer who messages about your listing at 9pm wants an answer that night, not when your office opens the next morning.

A chatbot doesn't replace your agents. It makes sure no lead goes cold because nobody was free at the moment they reached out.

What the bot does

The bot handles the whole first touch:

  1. Welcomes the lead with a greeting in your agency's voice
  2. Identifies intent: buying, renting, selling or investing
  3. Qualifies with the right questions for that intent (budget, location, timeline, financing)
  4. Matches the lead to properties from your listings
  5. Captures the lead's name and email
  6. Offers your Calendly or Cal.com link to book a showing or a callback
  7. Logs the lead to a sheet with a structured profile
  8. Escalates hot leads ("I'm ready to make an offer") to an agent right away

The qualification framework

The questions your bot asks decide the quality of the leads your agents get. Here's a framework by intent.

For buyers

  • What type of property are you looking for? (apartment, house, commercial)
  • Which location or neighborhood do you prefer?
  • What's your budget range?
  • Are you financing or paying cash?
  • If financing, have you been pre-approved?
  • When do you want to buy? (now, 1 to 3 months, 3 to 6 months, just browsing)
  • How many bedrooms? Anything else you need?

For renters

  • Which property type and location?
  • What's your monthly budget?
  • When do you want to move in?
  • How many people will live there?
  • Any pets?

For sellers

  • What's the address of the property?
  • When do you want to sell?
  • Have you already listed with another agent?
  • Do you have a price in mind?

For investors

  • Which property type and area?
  • What's your budget, and are you financing?
  • Do you plan to rent the property out or resell it?
  • What's your timeline?

The bot asks these one at a time, like a person would. It never sends the whole list as a form.

Building the bot

Stack

  • Chatmaid Developers: sends and receives WhatsApp messages on your agency's own number
  • n8n: workflow automation, on n8n Cloud or self-hosted
  • OpenAI: a chat model for the conversation and for pulling a lead profile out of it (this guide uses gpt-6-luna)
  • Google Sheets: conversation memory and the lead list
  • Calendly or Cal.com: the booking link the bot shares

Before you start

  • A Chatmaid Developers account. In test mode, a number you add in the dashboard connects right away and sends are simulated, which is free. For real messages, subscribe, add your agency's number (or a dedicated bot number) in live mode and scan the QR code with WhatsApp, the same way you link WhatsApp Web.
  • An API key for each mode. New keys get the messages:send and messages:read scopes by default. Test keys (sk_test_) work without a subscription. Live keys (sk_live_) need an active one. Each key only sees the numbers in its own mode, and only live numbers receive messages from real leads.
  • n8n reachable over HTTPS, because Chatmaid only accepts HTTPS webhook URLs.
  • An OpenAI API key.
  • A Google Sheet with these tabs, each with this header row:
TabHeader row
Conversationsphone, at, lead_message, bot_reply
Leadsphone, name, email, intent, property_type, location_preference, budget_min, budget_max, timeline, financing, qualification_score, notes, updated_at
Listings (optional)type, location, bedrooms, price, features, status

System prompt

This is the core of the bot. Customize the bracketed parts. You'll paste it into the Build Prompt node in a moment:

You are Sofia, a professional and warm real estate assistant for [Agency Name] in [City/Region].

Your role:
- Welcome new inquiries warmly
- Understand what the lead is looking for (buying, renting, selling, or investing)
- Ask qualification questions ONE AT A TIME. Never put several questions in the same message
- Keep every message SHORT (2-4 sentences max; this is WhatsApp, not email)
- Reply in the same language the lead writes in
- Be conversational, not robotic
- If someone asks whether you are a person, say you are [Agency Name]'s automated assistant and offer to connect them with an agent
- Only mention properties from the catalog below. Never invent listings, prices or availability; if you are not sure, offer to have an agent confirm

Qualification priority (buyers):
1. Intent (buy/rent/sell/invest)
2. Property type and location
3. Budget range
4. Timeline
5. Financing status
6. Specific requirements

When you have intent + location + budget + timeline, you have enough to qualify the lead.

After qualification:
- Mention 1-2 relevant properties from the catalog below if any fit
- Offer to book a showing: "Would you like to schedule a visit? Here's our booking link: [Calendly URL]"
- Ask for their name and email for follow-up

Escalate immediately if the lead:
- Says they're ready to make an offer
- Is a cash buyer with a short timeline
- Asks to speak with an agent directly

To escalate, start your message with "[ESCALATE]", then write a short reply telling the lead an agent will contact them shortly.

Property catalog:
[Paste 5-10 current listings in plain text: type, location, bedrooms, price, key features]

Agency info:
- Name: [Agency Name]
- Website: [URL]
- Office hours: Monday to Friday, 9am to 6pm
- Emergency line: [phone]

The prompt goes inside a JavaScript template literal, so keep backticks and ${ out of it.

The workflow at a glance

The nodes run in one line. Node names matter, because later nodes refer to earlier ones by name, so name yours the same way or update the expressions:

Chatmaid Webhook -> Verify Signature -> Leads Only -> Extract Message -> Get History
-> Build Prompt -> AI Response -> Clean Reply -> Send Reply -> Save Turn
-> Build Extraction -> Extract Lead -> Parse Lead -> Save Lead
-> Hot Lead? -> Build Alert -> Alert Agent

For a general-purpose version without the real estate parts, see how to build a no-code WhatsApp sales agent.

n8n workflow structure

Node 1: "Chatmaid Webhook" (Webhook). Set HTTP Method to POST, Path to chatmaid-leads and Respond to Immediately. Copy the Production URL into the Webhooks page of the Chatmaid dashboard, tick Message Received (message.received) and save. When you save, the dashboard shows your webhook secret, so copy it right away. n8n only listens on the Production URL while the workflow is published.

Add a Code node called "Verify Signature" right after this node. The n8n customer service agent guide has the code: it checks the X-Chatmaid-Signature header against that secret and passes the request through unchanged.

Node 2: "Leads Only" (If). Keep only one-to-one messages from leads, and skip your own agents, who will reply to the bot's alerts. Use this expression with the Boolean operator is true:

{{ $json.body.event === 'message.received' && $json.body.data.isGroup === false && !['+15550001111'].includes($json.body.data.from) }}

List every agent number that receives alerts in the brackets.

This also drops the dashboard's test event, which arrives as message.sent. Connect the false output to a No Operation, do nothing node.

Node 3: "Extract Message" (Edit Fields). Add these string fields:

  • leadPhone: {{ $json.body.data.from }}
  • leadMessage: {{ $json.body.data.content || '' }}
  • messageType: {{ $json.body.data.type }}
  • inboundId: {{ $json.body.data.messageId }}
  • receivedAt: {{ $json.body.data.receivedAt }}

from is the lead's number in E.164 format, such as +15557654321, and to is your connected number.

Node 4: "Get History" (Google Sheets, Get Row(s)). Read the Conversations tab with a filter on column phone and value {{ $json.leadPhone }}. Under Options, set When Filter Has Multiple Matches to Return All Matches. In the node's Settings, turn on Always Output Data, so a brand-new lead with no history still continues to the next node.

Node 5: "Build Prompt" (Code). Leave the mode on Run Once for All Items and paste the system prompt into SYSTEM_PROMPT:

const MODEL = 'gpt-6-luna';
const SYSTEM_PROMPT = `Paste the system prompt from the previous section here.`;

const msg = $('Extract Message').first().json;

// A new lead has no rows, so Get History outputs one empty item
const history = $('Get History').all()
  .map((item) => item.json)
  .filter((row) => row.lead_message && row.bot_reply)
  .slice(-10)
  .flatMap((row) => [
    { role: 'user', content: String(row.lead_message) },
    { role: 'assistant', content: String(row.bot_reply) },
  ]);

const leadMessage = msg.leadMessage
  || `[The lead sent a message of type ${msg.messageType} with no text]`;

return [{
  json: {
    ...msg,
    leadMessage,
    history,
    openaiBody: {
      model: MODEL,
      reasoning_effort: 'low',
      messages: [
        { role: 'system', content: SYSTEM_PROMPT },
        ...history,
        { role: 'user', content: leadMessage },
      ],
    },
  },
}];

Sending the last 10 exchanges lets the model see what it already asked, so it moves on to the next question. How to add conversation memory to your WhatsApp AI agent compares other ways to store it.

The model name lives in one place, MODEL, and the extraction step reuses it. As of October 2026, gpt-6-luna is the model OpenAI's deprecations page names as the replacement for GPT-4o, which shuts down on January 14, 2027. It is a reasoning model, and a low reasoning effort keeps replies quick. If you switch to a model without reasoning, delete the reasoning_effort lines, and test a few conversations after any model change.

Node 6: "AI Response" (HTTP Request). POST to https://api.openai.com/v1/chat/completions. For authentication, choose Generic Credential Type, Header Auth, with the name Authorization and the value Bearer followed by your OpenAI key. Turn on Send Body, choose JSON and Using JSON, and put this in the JSON field:

{{ JSON.stringify($json.openaiBody) }}

Node 7: "Clean Reply" (Edit Fields). Detect the escalation tag and strip it from the text the lead will see:

  • replyText (String): {{ $json.choices[0].message.content.replace('[ESCALATE]', '').trim() || 'Thanks! One of our agents will contact you shortly.' }}
  • escalate (Boolean): {{ $json.choices[0].message.content.trim().startsWith('[ESCALATE]') }}

Node 8: "Send Reply" (HTTP Request). POST to https://developers-api.chatmaid.net/v1/messages/send. Use Header Auth with the name Authorization and the value Bearer sk_live_xxx (your sk_test_ key while you build). Turn on Send Body, choose JSON and Using Fields Below:

NameValue
fromPhoneId+15551234567
to{{ $('Build Prompt').first().json.leadPhone }}
content{{ $('Clean Reply').first().json.replyText }}
idempotencyKey{{ $('Build Prompt').first().json.inboundId }}

fromPhoneId is your linked number in E.164 format, or its ID from GET /v1/phone-numbers. The idempotencyKey means a retried webhook gets back the message already sent instead of texting the lead twice.

Node 9: "Save Turn" (Google Sheets, Append Row). Append to the Conversations tab with Mapping Column Mode set to Map Each Column Manually:

ColumnValue
phone{{ $('Build Prompt').first().json.leadPhone }}
at{{ $('Build Prompt').first().json.receivedAt }}
lead_message{{ $('Build Prompt').first().json.leadMessage }}
bot_reply{{ $('Clean Reply').first().json.replyText }}

Under Options, set Cell Format to Let n8n format. With the default, Let Google Sheets format, Google Sheets turns +15557654321 into a number, and the Get History filter stops matching.

Lead capture and CRM logging

After 3 or 4 turns, the conversation holds enough to build a lead record. The next nodes extract a structured profile on every turn, so the record fills in as the lead answers. That's one extra model call per message.

Node 10: "Build Extraction" (Code, Run Once for All Items):

const EXTRACTION_PROMPT = `Extract the lead's details from this real estate conversation.
Only fill in values the lead stated clearly; use null or "unknown" otherwise.
qualification_score is "hot" if the lead is ready to act now, "warm" if they are
actively looking, and "cold" if they are browsing.`;

const nullable = (type) => ({ type: [type, 'null'] });
const oneOf = (...values) => ({ type: 'string', enum: values });
const LEAD_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  properties: {
    name: nullable('string'),
    email: nullable('string'),
    intent: oneOf('buy', 'rent', 'sell', 'invest', 'unknown'),
    property_type: oneOf('apartment', 'house', 'commercial', 'land', 'unknown'),
    location_preference: nullable('string'),
    budget_min: nullable('number'),
    budget_max: nullable('number'),
    timeline: oneOf('immediate', '1-3 months', '3-6 months', 'browsing', 'unknown'),
    financing: oneOf('pre-approved', 'in process', 'cash', 'unknown'),
    qualification_score: oneOf('hot', 'warm', 'cold'),
    notes: nullable('string'),
  },
};
LEAD_SCHEMA.required = Object.keys(LEAD_SCHEMA.properties);

const turn = $('Build Prompt').first().json;
const reply = $('Clean Reply').first().json.replyText;

const transcript = [
  ...turn.history,
  { role: 'user', content: turn.leadMessage },
  { role: 'assistant', content: reply },
]
  .map((m) => `${m.role === 'user' ? 'Lead' : 'Assistant'}: ${m.content}`)
  .join('\n');

return [{
  json: {
    openaiBody: {
      model: turn.openaiBody.model,
      reasoning_effort: 'low',
      response_format: {
        type: 'json_schema',
        json_schema: { name: 'lead', strict: true, schema: LEAD_SCHEMA },
      },
      messages: [
        { role: 'system', content: EXTRACTION_PROMPT },
        { role: 'user', content: transcript },
      ],
    },
  },
}];

Node 11: "Extract Lead" (HTTP Request). A copy of AI Response. It sends {{ JSON.stringify($json.openaiBody) }} the same way. response_format turns on OpenAI's structured outputs, so the reply is always a JSON object with exactly the fields in LEAD_SCHEMA.

Node 12: "Parse Lead" (Code, Run Once for All Items):

const parsed = JSON.parse($input.first().json.choices[0].message.content);

// Drop empty answers so a later turn never blanks a value captured earlier
const lead = Object.fromEntries(
  Object.entries(parsed).filter(([, value]) => value !== null && value !== 'unknown'),
);

return [{
  json: {
    ...lead,
    phone: $('Build Prompt').first().json.leadPhone,
    updated_at: new Date().toISOString(),
  },
}];

Node 13: "Save Lead" (Google Sheets, Append or Update Row). Point it at the Leads tab, match on the phone column, choose Map Automatically and set Cell Format to Let n8n format. The extracted fields share their names with the Leads header row, so each lead keeps one row that updates as the conversation goes. Empty answers are dropped in Parse Lead, so a later turn never blanks a value captured earlier, even after it has scrolled out of the last 10 exchanges. To use a CRM instead, swap this node for your CRM's n8n node, or forward the record through Zapier or Make.

Agent escalation notification

When the model tags a reply with [ESCALATE], your on-duty agent should hear about it on WhatsApp right away, with enough context to act without reading the thread. To route alerts by listing instead, add an agent_phone column to the Listings tab and use it as to in Alert Agent.

Node 14: "Hot Lead?" (If). Condition, Boolean is true:

{{ $('Clean Reply').first().json.escalate }}

Node 15: "Build Alert" (Code, Run Once for All Items), on the true branch:

const lead = $('Parse Lead').first().json;
const turn = $('Build Prompt').first().json;
const show = (value, fallback = 'unknown') => value ?? fallback;

const budget = lead.budget_min || lead.budget_max
  ? `${show(lead.budget_min, '?')} to ${show(lead.budget_max, '?')}`
  : 'not given';

const alertText = [
  'HOT LEAD: bot handoff',
  '',
  `Name: ${show(lead.name, 'not given')}`,
  `Phone: ${lead.phone}`,
  `Intent: ${show(lead.intent)}`,
  `Budget: ${budget}`,
  `Timeline: ${show(lead.timeline)}`,
  `Financing: ${show(lead.financing)}`,
  `Interest: ${show(lead.property_type)}, ${show(lead.location_preference, 'no area given')}`,
  '',
  `Last message: "${turn.leadMessage}"`,
  '',
  'Conversation: https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID',
  'Book a showing: https://calendly.com/your-agency/showing',
].join('\n');

return [{ json: { alertText } }];

Node 16: "Alert Agent" (HTTP Request). A copy of Send Reply with three changes: to is the agent's own WhatsApp number (for example +15550001111, not the number the bot runs on), content is {{ $json.alertText }}, and idempotencyKey is {{ 'alert_' + $('Build Prompt').first().json.inboundId }}.

The agent gets a message like this:

HOT LEAD: bot handoff

Name: Ana Ruiz
Phone: +15557654321
Intent: buy
Budget: 200000 to 250000
Timeline: immediate
Financing: pre-approved
Interest: apartment, Old Town

Last message: "I really liked the Old Town listing. Can I see it this week?"

Conversation: https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID
Book a showing: https://calendly.com/your-agency/showing

The agent can call or message the lead straight away, with the full profile in hand.

One refinement helps once agents start taking over:

  • Pause the bot when an agent replies. Subscribe to Message Sent From Phone (message.outgoing). It fires when someone types a reply on the linked phone itself, never for API sends. The customer service agent guide shows how to use it to stop the bot for that lead.

Handling common real estate questions

Teach the bot to answer these without escalating:

"Is the property still available?" The bot checks the catalog in its prompt and answers directly. Keep the catalog current (see the next section).

"What's included in the HOA?" Add HOA details to each listing in the catalog.

"Can I negotiate the price?" Give the bot a standard answer in the prompt, such as: "Our agents are open to discussing terms once we understand your offer. I can set up a call to talk through the details. Would that work?"

"What documents do I need?" Add a short document list for your market to the prompt.

Property catalog updates

The catalog in the prompt has to match your real listings. Two ways to keep it current:

Manual (simple): edit SYSTEM_PROMPT in the Build Prompt node whenever a listing changes.

Automated (advanced): keep listings in the Listings tab. Add a Google Sheets Get Row(s) node called "Get Listings" between Get History and Build Prompt, filter on status = available with Return All Matches, and turn on Execute Once in its Settings so it runs one time per message. Turn on Always Output Data too, so the bot keeps answering when nothing is available. Replace the catalog lines in the prompt with [CATALOG], then add this to Build Prompt below SYSTEM_PROMPT:

const catalog = $('Get Listings').all()
  .filter(({ json: l }) => l.type) // skip the empty item when nothing is available
  .map(({ json: l }) => `- ${l.type}, ${l.location}, ${l.bedrooms} bedrooms, ${l.price}: ${l.features}`)
  .join('\n');

const systemPrompt = SYSTEM_PROMPT.replace('[CATALOG]', catalog);

Use systemPrompt instead of SYSTEM_PROMPT in the messages array. A listing you add to the sheet shows up in the bot's next answer.

What to measure

Every agency's numbers differ, so measure your own from the first week. Everything you need is in the two sheets and n8n's Executions tab:

  • Response time: the gap between receivedAt and the reply, from each execution
  • Qualification rate: the share of Leads rows with intent, location, budget and timeline filled in
  • Contact capture: the share of leads that gave a name and email
  • Showings booked: give the bot its own Calendly or Cal.com event type, so its bookings are counted separately
  • Escalations: how many leads reached an agent through Alert Agent, and how many of those became showings

If leads drop off at the same question, move it later or rephrase it in the prompt.

What the bot cannot do

Be clear about its limits:

  • It can't give virtual tours, though it can share a link to a video tour.
  • It can't see photos or hear voice notes that leads send. As of October 2026, the webhook carries the message type and any caption, not the file.
  • It can't negotiate or make commitments.
  • It answers each message on its own. When a lead sends several messages in a row, each one gets its own reply.
  • It can't read live MLS data without an extra integration.
  • It can't sign documents or take payments.

These are all jobs for people, and that's the point. The bot filters and qualifies so your agents spend their time on the conversations that matter. For the follow-ups agents send after a showing, see the WhatsApp scheduling playbook for real estate agents.

Test and go live

Step 1: Check that sends work

Test-mode sends are simulated, so nothing reaches WhatsApp:

curl -X POST https://developers-api.chatmaid.net/v1/messages/send \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "fromPhoneId": "+15551234567",
    "to": "+15557654321",
    "content": "Hi! Thanks for asking about the listing. Are you looking to buy or rent?"
  }'

Step 2: Send a sample lead message

Click Execute workflow on the canvas so the Webhook node starts waiting for a test event (Listen for test event inside the node only captures the request and does not run the nodes after it). Deactivate Verify Signature for this run, and post a sample lead message to the Test URL:

curl -X POST https://your-n8n-host/webhook-test/chatmaid-leads \
  -H "Content-Type: application/json" \
  -d '{
    "event": "message.received",
    "timestamp": "2026-10-22T21:04:12.000Z",
    "data": {
      "messageId": "inmsg_test0001",
      "from": "+15557654321",
      "to": "+15551234567",
      "content": "Hi, is the 2-bedroom on Main Street still available?",
      "type": "text",
      "isGroup": false,
      "groupId": null,
      "receivedAt": "2026-10-22T21:04:11.000Z"
    }
  }'

Change messageId each time you resend it (inmsg_test0002, inmsg_test0003, and so on). Send Reply uses it as the idempotency key, and a repeated key returns the first reply instead of sending a new one.

Step 3: Go live

Inbound messages only exist on live connections. Subscribe, link your agency's number in live mode, and create a sk_live_ key. Switch Send Reply and Alert Agent to that key and the live fromPhoneId, turn Verify Signature back on and publish the workflow. Then message your number from another phone and watch the run in Executions.

The sandbox is free, and production is $7.99 a month, flat. See pricing. The webhook guide and the webhooks reference cover every event and field.

Frequently asked questions

Do I need the WhatsApp Business API or Meta verification?

No. Chatmaid links the number your agency already uses as a linked device, the way WhatsApp Web does, so there's no Meta Business verification step.

Can I build and test the bot for free?

Mostly. Chatmaid's sandbox key is free and simulates sends, and you can run the whole workflow with a sample webhook. OpenAI bills the model calls (two per message) even while you test. Receiving real messages from leads needs a live number, and live mode needs the $7.99 a month subscription.

What does it cost to run?

Chatmaid Developers is $7.99 a month flat for production, and the sandbox is free. You also pay for n8n (n8n Cloud, or your own server) and OpenAI usage. This workflow makes two model calls per lead message: one for the reply and one for the lead profile. Check n8n's pricing and OpenAI's gpt-6-luna model page before you budget.

Will the bot reply in WhatsApp groups?

Not with the Leads Only filter in this guide. Group messages arrive with isGroup set to true and are dropped, so the bot only talks to leads one to one.

Will leads know they're talking to a bot?

If they ask, yes. The system prompt above tells the bot to say it is your agency's automated assistant and to offer an agent, so keep that line when you customize it.

Can my number get restricted for running a chatbot?

It's very unlikely with normal use. WhatsApp restricts accounts that behave like spam, such as unsolicited bulk messages to people who don't have you saved. A bot that only answers leads who wrote to you first is normal use.