Skip to content
Dragmo Labs
Khatri Real Estate

Two Channels, One Agency: A Voice Agent And A WhatsApp Inbox For Khatri Real Estate

Khatri Real Estate's phone and WhatsApp lines are the two places a buyer's enquiry actually arrives. Both now run through AI agents that answer immediately, qualify the buyer against real inventory, and hand the team a lead ready to act on, with a human able to take over either channel by hand at any moment.

A real estate agency in Dubai, handling off plan launches, current inventory, and resale.

03Systems Built
05Verified Fixes
07Technologies

The Challenge

A real estate enquiry does not wait for office hours. It arrives as a phone call during a viewing, or as a WhatsApp message stacked behind a dozen others in an inbox nobody can read in real time. Khatri needed both channels answered immediately, with real inventory knowledge behind the answer, not a generic hold message or a keyword matching bot.

The harder requirement was qualification, not just response. A reply that does not gather area, budget, bedrooms, and timeline is not a lead, it is a transcript. And whatever answered a channel had to hand off cleanly to a human the moment a conversation genuinely needed one, without losing the thread of what had already been said.

What We Built

01

The WhatsApp system is a single Next.js application that owns the entire path: webhook receipt, signature verification, media handling (text, voice notes transcribed with Whisper, images, documents), the agent's own tool calling reasoning loop, reply delivery, and a live web inbox, backed by Supabase Postgres with Row Level Security and Realtime.

02

The agent runs a two stage qualification: first the questions the intent actually requires (property type, area, bedrooms, budget, timeline), only then contact details, so a general question is never forced through a full intake form. A lead only ever gets submitted once the state genuinely changes, tracked in an append-only leads table rather than trusted to the model's own memory of what it has already done.

03

The owner keeps a Detach control on every single conversation: WhatsApp Desktop style, one click hands a conversation to a human, and the AI stays silent on that contact until handed back, with every message still logged so re-attaching later is never amnesiac about what was said in between.

04

The voice agent answers the same business's phone line directly, drawing on the same off plan, current inventory, and resale knowledge, qualifying the caller conversationally, and reading their phone number back digit by digit before logging a fully populated lead row live during the call.

Verified, Not Just Claimed

Every fix, found and verified on the live system

Verified fixes
khatri-real-estate
01

One customer message answered three times, and the company inbox received three identical lead emails for a single enquiry.

Why it happened

A full agent turn (retrieval, a tool call, a second model round trip) routinely took 15 to 30 seconds. The provider's own webhook rules call anything over 10 seconds slow and retry on a 10s / 30s / 5min ladder, and nothing in the handler recognised a retry as a delivery it had already seen, so every retry re-ran the entire pipeline from scratch.

What changed

The provider's own delivery id became a database primary key, claimed before any work starts. A second delivery of the same id fails the insert and is dropped immediately. The actual reply now runs after the HTTP response is already sent, so the provider gets its fast acknowledgement and stops retrying, while the agent still gets its full 15 to 30 seconds to think.

Verified against

Replaying the identical delivery id twice through a signed test webhook produced one inbound row, one AI reply, and one email on the first call, and a no-op response with zero further writes on the second, checked directly against the live database.

02

Once a customer had given everything needed for a booking, the agent kept declaring their details had been sent again on later turns where nothing had changed.

Why it happened

Tool calls are not part of the conversation memory the system persists, so every reply was built from scratch with no record the lead tool had ever been called before. The rule was 'submit once everything is present,' and once that became true it stayed true on every following turn.

What changed

The submission decision moved out of the model's memory and into an append-only leads table in the database. Every reply is now told, in plain language, exactly what was last submitted for that contact and instructed not to submit again unless something has actually changed.

Verified against

A fresh conversation was qualified, submitted once, told 'no changes needed' twice with zero further sends, then had its budget changed, which correctly produced a 'why did this change' question with no send, and only sent again once a reason was given.

03

Asked what the agency is called, the agent refused to answer, even though the company's name sits in the browser tab title and on the WhatsApp profile the customer was already messaging.

Why it happened

Retrieval returned the numerically closest chunk to the question, which happened to be a caution rule about never stating an unlisted detail, written to guard a licence number or a direct line. The prompt never stated the company's own name as a plain fact, so the model generalised that caution to cover its own identity.

What changed

The company's name now sits directly in the system prompt as a known, always-safe fact, with an explicit note that the 'don't state unlisted details' rule governs licence numbers and direct phone lines, not the company's own identity.

Verified against

The exact question that failed was re-sent through the live pipeline after the fix and answered correctly.

04

The agent had total amnesia on every single message. Code compiled and ran with no error at any point.

Why it happened

The conversation-memory table stored each turn as a flat object with the message text directly on one field. The code reading it assumed a more commonly documented nested shape one level deeper, so every read silently returned empty history with nothing to point at the cause.

What changed

Writes were checked against the table's actual real rows rather than a documented convention. Reads now accept either shape, so both the existing rows and any newly written ones work uniformly.

Verified against

Confirmed by inspecting the table's live rows directly, not by trusting the shape that was assumed at first.

05

In an early build, every inbound WhatsApp message would have silently disappeared in production, with no error surfaced anywhere.

Why it happened

The authentication layer's route matcher did not exclude API routes, so an unauthenticated call to the webhook endpoint, which is every call, since the provider never carries a login session, was silently redirected to the login page instead of ever reaching the handler.

What changed

An explicit early-return guard for any API path was added inside the authentication check itself, plus a matching exclusion in the route matcher, so the same failure cannot come back if either safeguard alone is later reverted.

Verified against

Found by calling the endpoint directly and noticing the redirect rather than the expected response, ahead of production use.

Our WhatsApp used to feel impossible to keep up with, enquiries stacked up faster than anyone could read them. Now every message gets answered straight away, and by the time one of our team looks at a lead, the details are already there.
HHaseebKhatri Real Estate
Next.js 16, App Router, Turbopack/React 19, TypeScript strict/Supabase Postgres, Row Level Security, Realtime/Supabase Auth, Supabase Storage/YCloud (WhatsApp Business Platform)/OpenAI gpt-4o-mini, text-embedding-3-small, whisper-1/Nodemailer over SMTP