DEV Community

Cover image for Turn Messy WhatsApp Messages Into Structured JSON-Reliably
Seryl Lns
Seryl Lns

Posted on • Edited on

Turn Messy WhatsApp Messages Into Structured JSON-Reliably

Designing Deterministic Outputs From Unstructured Messages

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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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."
}
Enter fullscreen mode Exit fullscreen mode

One message → two actions. No follow-up questions needed.

worflow action

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"]
    send_whatsapp_message(
      session_id: payload["data"]["session_id"],
      text: output["suggested_reply"]
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

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?"
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "intent": "system.noop.skip",
  "confidence": 0.9,
  "actions": [],
  "suggested_reply": null,
  "analysis_summary": "The message is unrelated to hotel services."
}
Enter fullscreen mode Exit fullscreen mode

No action taken. No hallucinated response. No made-up answer about Paris weather. The system knows what it doesn't handle and stays silent.

Agent noop

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)
Enter fullscreen mode Exit fullscreen mode

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%+ (on our hotel dataset)
Handles typos/slang Poorly Well
Backend integration Custom per-flow Standard webhook

This pipeline works because it treats messages as data extraction problems, not conversations. Define the shape, let the LLM fill it, validate before acting. If you want to skip building it yourself:

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)