AI/ML14 min read · 8 April 2026

Building an Agentic Booking System with LangGraph & Twilio WhatsApp

A deep dive into BookingForge AI — a multi-tenant agentic booking SaaS where a LangGraph agent handles the entire booking flow over WhatsApp, from intent detection to slot confirmation.

LangGraphTwilioWhatsAppFastAPIAgentsMulti-tenant

Why WhatsApp for Bookings?

Most UK small businesses still take bookings by phone or email. WhatsApp has 2 billion users — building a booking agent that lives in WhatsApp means zero friction for customers. No app download, no account creation. Just message and book.

The Architecture

Customer WhatsApp → Twilio → FastAPI Webhook → LangGraph Agent → Supabase
                                                      ↓
                                              Twilio → WhatsApp Reply

LangGraph Agent Design

The booking agent uses a state machine with 5 nodes:

python
from langgraph.graph import StateGraph, END
from typing import TypedDict, Optional

class BookingState(TypedDict):
    phone: str
    tenant_id: str
    messages: list
    intent: Optional[str]
    service: Optional[str]
    date: Optional[str]
    slot_id: Optional[int]
    confirmed: bool

def build_booking_graph():
    graph = StateGraph(BookingState)
    
    graph.add_node("detect_intent", detect_intent_node)
    graph.add_node("collect_service", collect_service_node)
    graph.add_node("show_slots", show_slots_node)
    graph.add_node("confirm_booking", confirm_booking_node)
    graph.add_node("handle_cancel", handle_cancel_node)
    
    graph.set_entry_point("detect_intent")
    
    graph.add_conditional_edges("detect_intent", route_intent, {
        "book": "collect_service",
        "cancel": "handle_cancel",
        "unknown": END,
    })
    
    graph.add_edge("collect_service", "show_slots")
    graph.add_edge("show_slots", "confirm_booking")
    graph.add_edge("confirm_booking", END)
    
    return graph.compile()

Intent Detection Node

python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

async def detect_intent_node(state: BookingState) -> BookingState:
    last_msg = state["messages"][-1]
    
    result = await llm.ainvoke([
        {"role": "system", "content": """
            Classify the user's intent as one of: book, cancel, unknown.
            Reply with just the intent word.
        """},
        {"role": "user", "content": last_msg}
    ])
    
    state["intent"] = result.content.strip().lower()
    return state

Twilio WhatsApp Webhook

python
from fastapi import FastAPI, Form
from twilio.rest import Client

app = FastAPI()
twilio_client = Client(TWILIO_SID, TWILIO_TOKEN)

@app.post("/webhook/whatsapp")
async def whatsapp_webhook(
    From: str = Form(...),
    Body: str = Form(...),
    To: str = Form(...),
):
    phone = From.replace("whatsapp:", "")
    tenant_id = get_tenant_from_number(To)
    
    # Get or create conversation state
    state = await get_conversation_state(phone, tenant_id)
    state["messages"].append(Body)
    
    # Run the agent
    agent = build_booking_graph()
    new_state = await agent.ainvoke(state)
    
    # Save state for next message
    await save_conversation_state(phone, tenant_id, new_state)
    
    # Send reply
    reply = generate_reply(new_state)
    twilio_client.messages.create(
        from_=To,
        to=From,
        body=reply
    )
    
    return {"status": "ok"}

Multi-Tenancy

Each business gets their own WhatsApp number via Twilio. The To field in the webhook identifies which tenant the message is for:

python
def get_tenant_from_number(whatsapp_number: str) -> str:
    number = whatsapp_number.replace("whatsapp:", "")
    tenant = db.query(Tenant).filter(
        Tenant.whatsapp_number == number
    ).first()
    return tenant.id if tenant else None

Conversation State Persistence

Between messages, state is stored in Redis:

python
import redis
import json

redis_client = redis.from_url(REDIS_URL)

async def get_conversation_state(phone: str, tenant_id: str) -> BookingState:
    key = f"conv:{tenant_id}:{phone}"
    data = redis_client.get(key)
    if data:
        return json.loads(data)
    return BookingState(phone=phone, tenant_id=tenant_id, messages=[], confirmed=False)

async def save_conversation_state(phone: str, tenant_id: str, state: BookingState):
    key = f"conv:{tenant_id}:{phone}"
    redis_client.setex(key, 3600, json.dumps(state))  # 1hr TTL

Example Conversation

Customer: Hi, I want to book a canopy clean
Agent: Hi! I'd love to help you book. What type of service do you need?
       1. Basic Clean (£89)
       2. Commercial Package (£149)
       3. Deep Clean Pro (£249)

Customer: 2
Agent: Great choice! Here are available slots this week:
       • Mon 14 Apr - 9:00 AM
       • Tue 15 Apr - 2:00 PM
       • Wed 16 Apr - 11:00 AM
       Reply with your preferred slot number.

Customer: 1
Agent: Perfect! Confirming your booking:
       Service: Commercial Package (£149)
       Date: Monday 14 April, 9:00 AM
       Reply YES to confirm or NO to cancel.

Customer: YES
Agent: ✅ Booking confirmed! Reference: CC-2026-0042
       You'll receive a reminder 24 hours before your appointment.

BookingForge AI is currently in active development — the WhatsApp agent is live in testing with 2 pilot businesses.

MH
Mahmudul Hassan Mithun
AI SaaS Builder · BSc Data Science & AI, UEL · Building ContentForge AI

Related Posts

Building a Production Multi-Agent AI Pipeline with LangChain & FastAPI
Building a Production Multi-Agent AI Pipeline with LangChain & FastAPI
12 min read →
Deploying FastAPI to Railway: The Production Checklist
Deploying FastAPI to Railway: The Production Checklist
8 min read →