Dialogflow CX is excellent at understanding intent and managing conversation state. But out of the box, its WhatsApp integration sends only plain text. Real WhatsApp bots need buttons so users can reply without typing, lists for multi-option menus, CTA links to open URLs, and Flows for collecting structured data — none of which Dialogflow CX can produce on its own.
Meta documents a rich, well-specified JSON shape for each interactive message type on the WhatsApp Cloud API. The natural question is: can you author that JSON once, in Meta’s exact shape, and have it delivered to the customer without any translation layer in between?
With Baat, yes. This article shows how to paste Meta’s interactive JSON directly into a Dialogflow CX custom payload and have it reach the customer’s phone unmodified — along with how to handle the replies that come back.
The channel-wrap convention #
Baat reads Dialogflow CX custom payloads and looks for a top-level key that identifies the target channel. For WhatsApp Cloud API messages the key is BAAT_WAC_API. Everything inside that key is passed directly to the Graph API.
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Would you like to confirm your order?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "yes", "title": "Yes, confirm" } },
{ "type": "reply", "reply": { "id": "no", "title": "No, cancel" } }
]
}
}
}
}
That is the entire wrapper. The inner object is exactly what Meta’s documentation shows. Baat does not translate field names, does not re-shape the action block, and does not need a code update when Meta adds a new interactive type — it forwards what you give it.
Parse the custom payload → extract the value at
BAAT_WAC_API→ POST it tograph.facebook.com/v20.0/{phone_id}/messagesas the message body. No DTO, no translation.
The full interactive envelope Baat sends to Meta looks like this — Baat fills in messaging_product, to, and type; everything else is yours:
{
"messaging_product": "whatsapp",
"to": "919876543210",
"type": "interactive",
"interactive": { ...your object... }
}
Anatomy of a Meta interactive message #
Every interactive message shares the same outer shape:
{
"type": "interactive",
"interactive": {
"type": "button | list | cta_url | flow | product | product_list | location_request_message | address_message",
"header": { "type": "text | image | video | document", "text": "..." },
"body": { "text": "Main message text (required for most types)" },
"footer": { "text": "Optional footer line" },
"action": { ... }
}
}
The type field inside interactive determines which action shape is valid. The sections below show a working Dialogflow CX custom payload for each type.
1. Reply buttons #
The quickest win — up to 3 tap-to-reply buttons. No typing required from the user.
Custom payload #
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "button",
"body": {
"text": "Your order #48291 is packed and ready to ship. Confirm dispatch?"
},
"footer": { "text": "Reply expires in 24 hours" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "confirm_dispatch", "title": "Yes, dispatch" } },
{ "type": "reply", "reply": { "id": "hold", "title": "Hold for now" } },
{ "type": "reply", "reply": { "id": "cancel_order", "title": "Cancel order" } }
]
}
}
}
}
What comes back #
When the user taps a button, Baat receives a button_reply interactive message and maps the tapped button’s id to the session parameter baat_reply_id. Use it as a route condition in Dialogflow CX:
Route condition: .params.baat_reply_id = "confirm_dispatch"
title is limited to 20 characters. Titles longer than 20 characters are silently truncated by Meta and may look broken on the customer’s screen.
2. List message #
A scrollable list with sections and rows. Better than buttons when you have more than 3 options.
Custom payload #
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "What can I help you with today?" },
"footer": { "text": "Select an option below" },
"action": {
"button": "View options",
"sections": [
{
"title": "Orders",
"rows": [
{ "id": "track_order", "title": "Track my order", "description": "Get live delivery status" },
{ "id": "cancel_order", "title": "Cancel an order", "description": "Cancel before it ships" },
{ "id": "edit_address", "title": "Edit address", "description": "Change delivery address" }
]
},
{
"title": "Returns",
"rows": [
{ "id": "start_return", "title": "Start a return", "description": "Initiate a return or swap" },
{ "id": "refund_status", "title": "Refund status", "description": "Check refund progress" }
]
}
]
}
}
}
}
What comes back #
The selected row’s id is mapped to baat_reply_id, same as buttons:
Route condition: .params.baat_reply_id = "track_order"
3. CTA URL button #
A single button that opens a URL in the phone’s browser. Use it for order tracking links, payment pages, or any external destination.
Custom payload #
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": {
"text": "Your order is out for delivery. Tap below to track it live."
},
"footer": { "text": "Delivered by Delhivery" },
"action": {
"name": "cta_url",
"parameters": {
"display_text": "Track my order",
"url": "https://track.example.com/48291"
}
}
}
}
}
What comes back #
There is no structured reply for a CTA URL tap — WhatsApp does not notify you when a user opens the link. If you need to know whether the user clicked, use a UTM-tagged URL and check your analytics separately.
action.name field must be the string "cta_url" — not the URL itself. Omitting it or setting it to anything else causes a Meta validation error.
4. WhatsApp Flow #
Sends an interactive form that opens inside WhatsApp. The user fills it in without leaving the chat.
Custom payload #
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "flow",
"body": {
"text": "Book your appointment in under a minute — tap below."
},
"footer": { "text": "Takes less than 30 seconds" },
"action": {
"name": "flow",
"parameters": {
"flow_message_version": "3",
"flow_id": "1234567890123456",
"flow_cta": "Book now",
"flow_action": "navigate",
"flow_token": "booking-session-abc123",
"flow_action_payload": {
"screen": "APPOINTMENT_SCREEN"
}
}
}
}
}
}
flow_id vs flow_name: both are accepted. If you pass a flow_name string instead of a numeric flow_id, Baat resolves the name to the ID before sending.
flow_token: if you supply your own flow_token (as in the example above), Baat uses it verbatim — it is not overwritten. This gives you deterministic correlation: set flow_token to your Dialogflow session ID and you can tie the async Flow submission back to the originating conversation with certainty.
What comes back #
Flow submissions arrive as an nfm_reply message type. Baat parses the response_json string and maps each submitted field to a session parameter using the field’s name attribute from your Flow JSON. For example, if your Flow has a TextInput named full_name, the value is available as .params.full_name in your Dialogflow CX routes.
The flow_token you sent is also returned — use it to correlate the submission if your flow was sent asynchronously (e.g. from a broadcast campaign).
flow_id must be a JSON string, not a number. Meta’s API returns a cryptic 400 error if you write "flow_id": 1234567890123456 (unquoted integer). Always quote it: "flow_id": "1234567890123456".
5. Product and product list #
Display one product or a curated list from your WhatsApp Commerce catalog — with native add-to-cart support.
Single product #
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "product",
"body": { "text": "Based on your order history, you might love this:" },
"action": {
"catalog_id": "your_catalog_id",
"product_retailer_id": "SKU-VITC-50ML"
}
}
}
}
Product list #
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "product_list",
"header": { "type": "text", "text": "Our bestsellers this week" },
"body": { "text": "Tap any product to view and add to cart." },
"action": {
"catalog_id": "your_catalog_id",
"sections": [
{
"title": "Skincare",
"product_items": [
{ "product_retailer_id": "SKU-VITC-50ML" },
{ "product_retailer_id": "SKU-NIAC-30ML" },
{ "product_retailer_id": "SKU-SPF50-100G" }
]
}
]
}
}
}
}
product_list, header.type must be "text". Meta rejects image or video headers on product list messages even though those header types are valid for other interactive types.
6. Location request and address message #
Two newer Meta types that Baat forwards through without any code changes on its side — an example of why the passthrough model matters as Meta’s API evolves.
Location request #
Prompts the user to share their GPS location using WhatsApp’s native location picker.
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "location_request_message",
"body": {
"text": "Please share your location so we can show availability in your area."
},
"action": {
"name": "send_location"
}
}
}
}
Address message #
Opens a structured address form inside WhatsApp — useful for collecting delivery addresses without a Flow.
{
"BAAT_WAC_API": {
"type": "interactive",
"interactive": {
"type": "address_message",
"body": {
"text": "Enter your delivery address to complete your order."
},
"action": {
"name": "address_message",
"parameters": {
"country": "IN"
}
}
}
}
}
Reply handling — closing the loop #
A bot is a loop: send interactive → receive reply → branch. The reply side is where most Dialogflow CX + WhatsApp tutorials fall short.
How Baat normalises replies #
| User action | WhatsApp type | Session parameter set by Baat |
|---|---|---|
| Taps a reply button | button_reply |
baat_reply_id = the button’s id |
| Selects a list row | list_reply |
baat_reply_id = the row’s id |
| Submits a Flow | nfm_reply |
Each field name from your Flow JSON is mapped to its own session parameter |
| Shares location | location |
baat_location_lat and baat_location_lng |
Branching in Dialogflow CX #
Use route conditions on the session parameters Baat sets. For button and list replies:
.params.baat_reply_id = "confirm_dispatch"
For Flow submissions, branch on individual field values:
.params.service_type = "spa"
For Flow correlation using flow_token: when a Flow is sent from a broadcast (asynchronously, outside a live session), the flow_token you supplied comes back in the webhook. Baat sets it as baat_flow_token in the session so you can match the submission to the originating customer record.
flow_token to the Dialogflow CX session ID (.id) when sending a Flow mid-conversation. This ties the async submission back to the exact session, so the conversation continues seamlessly even if the user takes a few minutes to fill in the form.
The error model #
Most “Dialogflow + WhatsApp” integrations silently discard malformed payloads and leave you staring at nothing. Baat’s behaviour is different:
- Validation at send time. If Baat detects a structural problem before calling the Graph API (missing required field, wrong type, unknown key), it creates a
FAILEDmessage entry in the agent inbox with the validator’s explanation instatus_message. - Meta rejections are preserved verbatim. If the Graph API returns a 400 or 4xx error, Baat persists Meta’s full response body — including Meta’s error code and message — as the
status_messageon the failed message. You see exactly what Meta said, not a generic “send failed”. - Delivery receipts flow back. For successful sends, Baat processes Meta’s
SENT/DELIVERED/READstatus webhooks and updates the message status in the conversation view.
Architecture overview #
For readers who want to understand what happens end-to-end:
- Dialogflow CX fulfillment webhook returns a response that includes a custom payload with the
BAAT_WAC_APIkey. - Baat’s message service receives the fulfillment response, locates the
BAAT_WAC_APIvalue, applies lightweight normalisation (e.g. coercing a plain stringbodyto{"text": "..."}if the author omitted the object wrapper), and persists the message. - The send worker posts the object to
https://graph.facebook.com/v20.0/{phone_id}/messageswithmessaging_product,to, andtypeadded by Baat. Theinteractivefield is your object, unchanged. - Status webhooks from Meta (
SENT,DELIVERED,READ,FAILED) are processed and stored against the message. - Incoming replies (button_reply, list_reply, nfm_reply) arrive on the Meta webhook endpoint, are normalised into session parameters, and forwarded to Dialogflow CX to continue the conversation.
Gotchas at a glance #
| Mistake | Symptom | Fix |
|---|---|---|
Fields placed at the top level of interactive instead of inside interactive.interactive |
Meta 400 — unknown field | The outer object needs type: "interactive" and an interactive sub-object; all fields go inside that sub-object |
Missing body.text |
Meta 400 — required field missing | body.text is required for button, list, cta_url, flow, product, and product_list |
flow_id passed as a JSON number |
Cryptic Meta 400 | Always quote it: "flow_id": "1234567890123456" |
product_list header type is image |
Meta rejects the message | Use "header": {"type": "text", "text": "..."} — media headers are not supported on product list |
| More than 3 reply buttons | Meta 400 | Use a list message instead when you need more than 3 options |
| More than 10 list rows across all sections | Meta 400 | Split into multiple messages or reduce options |
| Sending an interactive message as the first message to a user in a new 24-hour window | Meta error — template required | The first outbound message outside an active conversation window must be an approved template. Interactive messages only work within an active 24-hour session. |
The point #
You author once, in Meta’s shape. Baat does not impose an intermediate schema, does not lag behind Meta’s API roadmap field by field, and does not silently swallow your payload when something is wrong. When Meta ships a new interactive type, you can use it in your Dialogflow CX custom payload the same day — no waiting for a Baat release.