def works():
AIDef Works3 min read

Building a WhatsApp Sales Assistant That Actually Qualifies Leads

A practical walkthrough of building a WhatsApp assistant that does more than reply — it qualifies leads, captures the fields your sales team needs, and hands off cleanly to a human.

  • WhatsApp
  • AI Assistants
  • Lead Generation
  • Tool Calling
A hand holding a smartphone with a messaging conversation open
Photo: Karolina Grabowska www.kaboompics.com / Pexels

Most "AI on WhatsApp" projects stall at the same place: the bot answers questions politely, and then nothing happens. No lead captured, no field your CRM can use, no clean handoff. A sales assistant that qualifies is a different design problem — it has a goal, a shape of data it needs to collect, and a moment where it steps aside for a human.

This is a walkthrough of that design, model-agnostic by default. The same structure works whether you run Claude, GPT, or Gemini behind it — the model is a swappable component, not the architecture.

The three jobs of a qualifying assistant

Strip away the chat and a good sales assistant does exactly three things:

  1. Answer — respond to product and pricing questions accurately from your own content, not the model's imagination.
  2. Qualify — collect the handful of fields your sales team actually acts on: budget range, timeline, company size, use case.
  3. Route — recognise a qualified lead and hand it to a person or your CRM before the conversation goes cold.

The mistake is treating this as one giant prompt. The reliable version treats "qualify" as structured data collection with a tool, and "answer" as retrieval over your own knowledge base.

Architecture

WhatsApp talks to you through a webhook — the Cloud API (or a provider like Twilio) POSTs each inbound message to an endpoint you host. Your endpoint is where the real work happens:

  • Receive the message and load the conversation history for that phone number.
  • Call the model with your system prompt, the history, and a set of tools it can call.
  • Execute any tool the model asks for (look up a product, save a qualified lead), feed the result back, and send the model's reply to WhatsApp.

The one non-negotiable: reply to the webhook fast. WhatsApp retries if you take too long, and a slow model call inside the request will get you duplicate messages. Acknowledge the webhook immediately, then do the model work and send the reply as a separate outbound API call.

Qualification as a tool, not a vibe

Do not ask the model to "decide when a lead is qualified" in prose. Give it a tool with a strict schema and let the schema define what qualified means. The model fills the arguments as the conversation reveals them; when the required fields are present, you route.

ts
// The tool the model calls once it has enough to act on.
const saveLead = {
  name: 'save_qualified_lead',
  description:
    'Call this once you have collected the buyer\'s use case, ' +
    'rough budget, and timeline. Do not call it earlier.',
  input_schema: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      useCase: { type: 'string', description: 'What they want to build' },
      budgetBand: { type: 'string', enum: ['<5k', '5-25k', '25k+', 'unsure'] },
      timeline: { type: 'string', enum: ['now', 'this-quarter', 'exploring'] },
      handoffWanted: { type: 'boolean' },
    },
    required: ['useCase', 'budgetBand', 'timeline'],
  },
} as const

Because the schema marks three fields as required, the model naturally works the conversation toward collecting them before it can call the tool. You have encoded your sales team's definition of "worth a call" into a JSON schema — and you can change that definition without retraining anything.

The webhook loop

Here is the shape of the handler, trimmed to the parts that matter:

ts
export async function handleInbound(msg: InboundMessage) {
  if (await alreadyProcessed(msg.id)) return       // idempotency
  const history = await loadHistory(msg.from)

  let response = await model.chat({
    system: SALES_SYSTEM_PROMPT,
    messages: [...history, { role: 'user', content: msg.text }],
    tools: [lookupProduct, saveLead],
  })

  // Resolve any tool the model asked for, then let it finish its reply.
  while (response.toolCalls?.length) {
    const results = await Promise.all(
      response.toolCalls.map(runTool),        // runTool routes by name
    )
    response = await model.chat({
      system: SALES_SYSTEM_PROMPT,
      messages: [...history, ...response.turns, ...results],
      tools: [lookupProduct, saveLead],
    })
  }

  await sendWhatsApp(msg.from, response.text)
  await saveHistory(msg.from, response.turns)
}

Everything hard is in the tools, not the loop. lookupProduct does retrieval against your catalogue so answers are grounded. saveLead writes to your CRM and, if handoffWanted is true, pings the sales channel. The model just orchestrates.

Grounding answers so it does not invent pricing

A sales assistant that hallucinates a discount is a liability. Keep the model's product knowledge out of the prompt and in a retrieval tool that returns real records. If the tool returns nothing, the assistant should say it will check with the team — never guess. State that explicitly in the system prompt and test it with adversarial questions before launch.

Where the human comes back in

The best handoff is boring: the moment a lead is qualified, a real notification lands in the channel your sales team already watches, with the structured fields attached. No dashboard to check, no new habit to build. The assistant's job was never to close — it was to make sure a person talks to the right buyer with context already in hand.

The assistant that qualifies well is measured by the meetings it books, not the messages it sends.
Explore AI & Automation

Related articles

// Let's build

Want to apply this to your roadmap?

Bring us into the architecture, delivery, or AI planning conversation.