Writing(01) · Case study
Running a real booking flowinside WhatsApp
How IléHQ takes a guest from a first “hi” to a paid, verified booking without leaving the chat. Every number in this piece comes from the codebase.
In Lagos, short-let bookings mostly happen in WhatsApp DMs. A guest messages a host, they go back and forth about dates and price, someone sends a bank transfer, and everybody hopes. No availability calendar, no verification, no receipts, and disputes come down to screenshots. The habit is unbeatable, so IléHQ doesn’t fight it. The whole product lives where the guests already are.
That decision sounds friendly until you build it. WhatsApp gives you one text channel, no UI state, and a webhook. Everything a booking site does with forms, sessions and buttons has to be reconstructed behind a conversation, and the conversation is being run by a language model that must never be trusted with money.
The runtime is hostile
Twilio and Meta both redeliver a webhook when your response is slow, and an AI agent turn takes 15 to 30 seconds. Answer slowly and the same guest message arrives twice, which without protection means two bookings and two charges. So the webhook does almost nothing synchronously: opt-out checks, rate limiting (10 messages per phone per minute), and deduplication, then acknowledges Twilio and defers the real work. Dedup is an atomic check-and-set on a durable KV store keyed by message id with a five-minute TTL, and it deliberately fails open: if the store is unreachable, a guest message is processed anyway, because a dropped booking costs more than a rare duplicate we catch downstream.
From there an inbound message cascades through a six-layer routing pipeline: sync guards, special handlers, a session fast path, tenant resolution, and only then the AI entry and orchestrator. The same pipeline serves both Twilio and Meta’s Cloud API; the Meta webhook just normalizes its payload into the same shape before entering.
The model talks, the state machine decides
The booking itself is an explicit finite state machine with 11 states, from COLLECTING_PARAMS through AWAITING_VERIFICATION and AWAITING_PAYMENT to CONFIRMED, with explicit failure exits like PAYMENT_EXPIRED and ABANDONED. The model’s job is conversation: figuring out what the guest wants and calling tools. It has 38 of them, 29 for the short-let flow alone, things like check_availability, calculate_price, send_verification_code, create_booking, request_late_checkout. What it cannot do is invent a price, skip verification, or mark anything paid. Prices come from a pricing engine, availability from queries against real tickets, and payment state only ever changes when a Stripe or Paystack webhook says so.
The agent layer is also swappable behind a feature flag: a deterministic pipeline handles the well-trodden path, and a tool-calling agent takes over for everything messy, both speaking the same contract. When the v2 agent misbehaves, we flip an env var, not rewrite the flow.
Money in two markets
Payments are routed by market: Paystack for Nigeria in naira, Stripe for international cards in dollars. Webhook handlers use state-based idempotency, checking what the entity already is before mutating, backed by unique constraints on payment references at the database level. A booking and its damage deposit are charged as one grouped charge, and deposits are always platform-held, because refund control matters more than payout speed.
Host payouts are gated by trust tier. New hosts’ funds are held and paid out on schedule; established hosts get a Paystack subaccount split at charge time and are paid instantly. And the ugly edge case is handled honestly: if a guest pays after their payment window expired, the money is automatically refunded in full and the guest is told, instead of the platform quietly keeping a booking that no longer exists.
Trust is arithmetic, not vibes
Every guest carries a numeric trust score built from 13 concrete signals: a completed stay is +10, an on-time checkout +5, a no-show is -15, an upheld damage claim -30, a fraud report -50. The score maps to five tiers from BLOCKED to VIP, and the tiers change what the system asks of you, how much verification, whether a deposit is required. It’s crude on purpose. A rule you can explain to a host beats a model you can’t.
No queue, a cron fleet
There is no message queue. Long work runs after the webhook acknowledges, and everything time-based, booking expiry, no-show detection, overstay detection, deposit lifecycle, payment reconciliation, host payouts, iCal sync, lives in roughly 45 scheduled jobs behind a single dispatcher. Boring, observable, and every job is idempotent because with this many crons, one will eventually run twice.
Where it stands
The system is live in Lagos: 79 database models, 251 API routes, 38 AI tools, one person building it. The lesson I’d hand anyone doing conversational commerce: put the AI at the edge and the guarantees in the middle. The model is the interface. The database, the state machine and the payment webhooks are the product.
Building something conversational that has to move money? This is exactly the work I take on.
See services