Your users send messy, unstructured text. Your backend expects clean, validated JSON. Bridging that gap is the hard part — and most teams get it wrong.
This article walks through how to turn a raw WhatsApp message into a structured payload your application can safely act on.
The Problem
A real message from a hotel guest:
hey can u move bk-4521 to fri plzz?? also ned more towls in rm 312 thx
Typos. Abbreviations. Two requests in one message. No punctuation.
Your PMS expects:
{
"booking_ref": "BK-4521",
"new_date": "2026-03-14",
"room": "312",
"items": ["extra towels"]
}
How do you get from A to B — reliably?
Step 1: Define Your Schema
Before touching any LLM, define what your backend expects. This is your contract.
{
"intent": "string (enum)",
"actions": [
{
"type": "string (action identifier)",
"payload": {
"field_1": "string | number | null",
"field_2": "string | number | null"
}
}
],
"confidence": "number (0-1)",
"suggested_reply": "string | null"
}
Key principles:
- Finite set of intents — your agent only handles what you define
- Typed fields — each field has an expected type
-
Nullable — missing data is
null, not hallucinated - Confidence score — so your backend can decide when to auto-act vs. ask for confirmation
Step 2: Extract, Don't Converse
The classic mistake is building a multi-turn flow to gather missing fields. Instead, extract everything from the first message in a single LLM call.
The messy message:
hey can u move bk-4521 to fri plzz?? also ned more towls in rm 312 thx
Gets processed into:
{
"intent": "modify_booking",
"confidence": 0.93,
"actions": [
{
"type": "reservation.booking.reschedule",
"payload": {
"booking_ref": "BK-4521",
"new_date": "2026-03-14"
}
},
{
"type": "housekeeping.task.create",
"payload": {
"room": "312",
"items": "extra towels"
}
}
],
"suggested_reply": "Done! Booking BK-4521 moved to Friday. Extra towels are on the way to room 312."
}
One message → two actions. No follow-up questions needed.
Step 3: Validate Before Acting
Never trust raw LLM output blindly. Validate the payload before executing:
# Your webhook handler
def handle_agent_webhook(payload)
output = payload["data"]["output"]
# Check confidence threshold
return if output["confidence"] < 0.8
output["actions"].each do |action|
case action["type"]
when "booking.reschedule"
# Validate booking exists
booking = Booking.find_by(ref: action.dig("payload", "booking_ref"))
next unless booking
booking.reschedule!(new_date: action.dig("payload", "new_date"))
when "housekeeping.task.create"
HousekeepingTask.create!(
room: action.dig("payload", "room"),
items: action.dig("payload", "items")
)
end
end
# Send the suggested reply back via WhatsApp
if output["suggested_reply"]
whatsrb.sessions.send_message(
session_id: payload["data"]["session_id"],
text: output["suggested_reply"]
)
end
end
Your backend stays in control. The LLM proposes, your code decides.
Step 4: Handle the Edges
What about messages that don't match any intent?
"What's the weather like in Paris tomorrow?"
Output:
{
"intent": "system.noop.skip",
"confidence": 0.9,
"actions": [],
"suggested_reply": null,
"analysis_summary": "The message is unrelated to hotel services."
}
No action taken. No hallucinated response. No made-up answer about Paris weather. The system knows what it doesn't handle and stays silent.
This is the difference between a deterministic system and a chatbot that tries to answer everything. A chatbot would hallucinate a weather forecast. A message-driven system returns noop and moves on.
The Full Pipeline
Raw message
↓
Pre-filter (spam, noise → free, no LLM)
↓
Router (classify → which agent handles this?)
↓
Agent (LLM → structured payload with schema)
↓
Webhook (HMAC-signed → your backend)
↓
Validate (confidence check, field validation)
↓
Execute (create task, update booking, send reply)
Explore the full pipeline in detail →
Each layer adds reliability:
- Pre-filter removes noise before LLM cost
- Router ensures the right agent handles each message type
- Schema constrains LLM output to valid shapes
- Confidence lets you set auto-action thresholds
- Webhooks decouple processing from action
What You Get
| Metric | Typical chatbot | Message-driven |
|---|---|---|
| Round-trips per request | 3-5 | 1 |
| Latency | 5-15s (multi-turn) | 1-2s |
| State management | Complex | None |
| Accuracy on intent | ~70% | 95%+ |
| Handles typos/slang | Poorly | Well |
| Backend integration | Custom per-flow | Standard webhook |
Try It Yourself
WhatsRB Cloud handles this entire pipeline — from raw WhatsApp message to structured webhook. Define your intents, set your schema, and let the platform do the extraction.
Beta opens March 31, 2026.
Join the waitlist → | Read the API docs →
No chatbot framework. No dialog trees. Just clean data from messy messages.
Built with Ruby, Rails, and the belief that most messages don't need a conversation — they need an action.


Top comments (0)