WhatsApp Flows: interactive forms end-to-end
A WhatsApp Flow is an in-app form your customer completes inside WhatsApp — screens, dropdowns, date pickers, radio groups, and a submit button — without leaving the conversation. You use a Flow where a free-form back-and-forth is clumsy (collecting a structured appointment, a lead-capture form, a survey) but you do not want to pre-write a free text answer set. Orbit wires Meta’s Flows API end-to-end: build the JSON, upload it to Meta, publish it, send it as a button on an interactive message, and read submissions back. This guide covers the whole lifecycle. If you have not connected a WABA yet, run Get started with WhatsApp first.What a WhatsApp Flow is (and when it applies)
A Flow is a multi-screen form defined in JSON and hosted on Meta. The recipient taps a CTA button labelled by you — “Book an appointment”, “Complete your checkout” — and WhatsApp renders the screens natively. The answers return to you as one structured reply. The four rules that shape when you use a Flow:- No template review inside the 24h window. A Flow message is still an interactive message — it rides the 24-hour customer-service window the same way buttons and lists do (WhatsApp 24h freeform window). Inside the window you send it directly with no template approval. Outside the window you still need a template the customer replied to, or an approved template that opens the conversation.
- A Flow has declared categories. When you create the Flow you choose one or more of
APPOINTMENT_BOOKING,LEAD_GENERATION,CONTACT_US,CUSTOMER_SUPPORT,SURVEY,OTHER. Meta enforces the set at create time. - Publish is irreversible in Meta. A Flow starts as a
DRAFT; you upload its JSON, run Meta’s validation, then publish. Meta’svalidation_errorssurface on upload and publish — Orbit forwards them verbatim. - A submission returns a structured payload. The completed form replies as an
interactive.nfm_replyinbound; Orbit parses itsresponse_jsonand stores both the raw answers and the session correlation token (flow_token) you mixed in when you sent it.
Endpoint surface
Two route families operate on Flows. Everything below sits underhttps://api.orbit.devotel.io/api/v1/whatsapp and authenticates with the same X-API-Key header as the other WhatsApp endpoints.
Lifecycle (admin)
Create/publish/delete/upload are owner-admin operations. List/funnel/submissions/read are readable with any key.
Send a Flow inside the window
One POST sends the Flow as an interactive message. This is the route the dashboard’s Flow-send panel uses; call it directly from the API the same way.POST /api/v1/whatsapp/messages/send-flow
A successful send returns
{ messageId, status } and fans out a message.sent webhook event; the reply callback you actually care about is message.received with its interactive.nfm_reply payload (see Webhook events below).
Building the flow JSON
A Flow JSON document describes screens, components, and the routing between screens. Orbit pins the JSONversion to Meta’s current 5.0 (the server rejects any other value and rewrites it for you).
A typical three-screen Flow:
- Screens — the top-level
screens[]array. Each screen has anid, an optionaldatamap naming fields, and alayout.children[]tree of components (TextHeading,Dropdown,RadioButtonsGroup,TextInput,DatePicker,EmbeddedContent,Footer). - Routing — a Footer component’s
on-click-actionnames the next screen (navigate) or ends the flow (complete). A screen with no outgoing action is reachable only as the first screen; a screen with"terminal": trueis the last one. - Terminal nodes — the
completeaction’spayloadis whatnfm_reply.response_jsonreturns on submission. Interpolate entered fields with${form.<name>}or carry context forwarded from your send (${data.flow_token}).
next naming a screen that does not exist, a Footer without an action) Meta’s validation rejects the upload — see the next section.
Validating type-union errors the tests enforce
Meta enforces a closed set of literals for Flow category and status; Orbit ships that exact set in its exported types and pins it under test so a dropped member wakes the build. You see the same set echoed back when you create, list, and publish flows. The categories the API accepts onPOST /whatsapp/flows:
GET /whatsapp/flows/:flowId and in list responses):
DRAFT Flow also returns validation_errors from Meta — Orbit normalizes both the top-level validation_errors shape (asset upload) and the nested error.error_data.details string (publish) into the response’s error.details.validation_errors list, each item with { error, error_type, message, component?, code?, line?, column? }. Render those items in your dashboard the same way the Orbit dashboard does — they pinpoint the offending screen/component instead of a generic failed-publish toast.
Sending via curl and via dashboard
curl
body are auto-rewritten to tracked short links (same toggle as the main WhatsApp send path), so long-link attribution still flows.
dashboard
In the Orbit dashboard open Messages → WhatsApp → Flows. The list shows your WABA’s Flows; the button next to eachDRAFT runs validation, the button next to a published one opens Send flow. The Send panel is a thin form over the same flow_id / flow_cta / body / flow_token fields above — it forwards the connection you picked in the WABA picker as phone_number_id, so the send routes through the selected WABA instead of the org default.
Webhook events for flow completion
Two events matter for the round trip. Subscribe on Settings → Developer → Webhooks to both:
Orbit also dispatches a CDP event
whatsapp_flow_submitted for the dashboard’s contact-profile view — you do not subscribe to it; the Flow-submissions read endpoint is how you inspect it.
Inside the message.received payload the parsed form data lands under interactive as an nfm_reply:
interactive_type: "nfm_reply", flow_token (echo of what the form’s last screen interpolated from your send), and flow_response (JSON-parsed response_json minus the token). Match the inbound flow_token against the send’s flow_token to join a submission to the session you opened.
If submissions are arriving but the dashboard funnel shows them only as final screens (not full screen transitions), that is the funnel-vs-list distinction on Troubleshoot WhatsApp Flow submissions.
Rate-limit behavior and the wa-catalog bucket
Direct-send WhatsApp routes that bypass the richer message pipeline (/messages/send-flow, /messages/send-interactive, catalog product sends) share a single rate-limited bucket per tenant: 80 requests per minute, keyed on a tenant-suffixed :wa-catalog key. The bucket deliberately stays distinct from the main POST /messages path and from template or signup reads — a heavy catalog sender and a heavy Flow sender cannot crash each other on one marginal route, and one tenant cannot starve another on a shared NAT.
If you send Flows in bulk, spread them against a tenant-scoped limiter on your side; the burst limiter will reject the excess with HTTP 429 per request. Reply with a Retry-After header into your sender queue — the same pattern as the Send message endpoint main path.
When flow vs catalog vs template content
Pick the surface that matches what the customer does:
A Flow still needs the window to be open (or the customer to have replied to your template first). When you are outside the window, lead with an approved template that nudges the customer to reply, then send the Flow once the window opens.
See also
- Get started with WhatsApp — connect a WABA and send the first template.
- WhatsApp 24h freeform window — what governs sending the Flow direct.
- Troubleshoot WhatsApp Flow submissions — funnel-empty or
landed_in_cdp: falsediagnostics. - Webhook Events — full
message.sent/message.receivedpayload examples.