What is Flow JSON?
Flow JSON is the document that describes a WhatsApp Flow — the multi-screen native forms customers fill in inside a chat. It lists every screen, the components on each screen (headings, text, inputs, dropdowns, date pickers, a footer button), how the screens connect and what happens when the customer taps a button. WhatsApp renders it natively on the phone; you author it in Meta's Flow Builder or write it by hand and upload it through the Flows API.
The top level is small: version (the Flow JSON schema version the Flow compiles against, such as 7.3), screens (an array of screen objects), and — only for Flows that call an endpoint — data_api_version (4.0, released in November 2025, adds endpoint security improvements such as a signed flow token; 3.0 is still accepted) plus a routing_model mapping each screen to the screens it may open next. Each screen has a unique id (SUCCESS is reserved), an optional title, a data block declaring the dynamic fields it expects (each with an __example__), terminal: true if it ends the Flow, and a layout of type SingleColumnLayout whose children are its components.
Components bind to data with expressions: ${data.slots} reads data passed to the screen, ${form.name} reads what the customer entered, and since version 4.0 ${screen.APPOINTMENT.form.date} reads another screen's values. Buttons run actions: navigate opens another screen with a payload, data_exchange sends a payload to your Flow endpoint and shows the screen it returns, and complete ends the Flow and sends its payload to your webhook. Flow JSON is the static definition; the Flow endpoint serves the dynamic data that fills it at run time.
Key takeaways
- Flow JSON is the static definition of a WhatsApp Flow: screens, components, routing and actions in one versioned document.
- The top level needs
versionandscreens; Flows with an endpoint adddata_api_version(4.0 is recommended) and arouting_model. - Each screen has an
id, aSingleColumnLayoutof components, and a terminal screen must include a Footer. - Actions move the customer:
navigateto a screen,data_exchangeto your endpoint,completeto finish and submit. - Flow JSON content is capped at 10 MB, and the routing model allows at most 10 branches.
Explore a two-screen Flow JSON
{"version": "7.3","data_api_version": "4.0","routing_model": {"APPOINTMENT": ["DETAILS"],"DETAILS": []},"screens": [{"id": "APPOINTMENT","title": "Book a visit","data": {"departments": {"type": "array","__example__": [{"id": "ortho","title": "Orthodontics"}]}},"layout": {"type": "SingleColumnLayout","children": [{"type": "Dropdown","name": "department","label": "Department","data-source": "${data.departments}"},{"type": "DatePicker","name": "date","label": "Preferred date"},{"type": "Footer","label": "See free slots","on-click-action": {"name": "data_exchange","payload": {"department": "${form.department}","date": "${form.date}"}}}]}},{"id": "DETAILS","terminal": true,"sensitive": ["phone"],"layout": {"type": "SingleColumnLayout","children": [{"type": "TextInput","name": "phone","label": "Phone","input-type": "phone"},{"type": "Footer","label": "Confirm booking","on-click-action": {"name": "complete","payload": {"date": "${screen.APPOINTMENT.form.date}","phone": "${form.phone}"}}}]}}]}
Field 9: screens.1.layout.children.1.on-click-action.payload — complete ends the Flow; this payload reaches your webhook in the flow response message.
Key facts at a glance
Flow JSON: key facts and limits
- Required top level
- version, screens
- With an endpoint
- data_api_version (4.0 recommended, 3.0 accepted) plus routing_model
- Layout
- SingleColumnLayout, the only layout type
- Reserved screen id
- SUCCESS
- Routing
- At most 10 branches; every route ends at a terminal screen
- Size limit
- Flow JSON content up to 10 MB
- Core actions
- navigate, data_exchange, complete
- Cross-screen references
- ${screen.<id>.form.<field>} from version 4.0
Flow JSON in code
{"version": "7.3","data_api_version": "4.0","routing_model": {"APPOINTMENT": ["DETAILS"],"DETAILS": []},"screens": [{"id": "APPOINTMENT","title": "Book a visit","data": {"departments": {"type": "array","items": {"type": "object","properties": {"id": {"type": "string"},"title": {"type": "string"}}},"__example__": [{"id": "ortho","title": "Orthodontics"}]}},"layout": {"type": "SingleColumnLayout","children": [{"type": "Dropdown","name": "department","label": "Department","required": true,"data-source": "${data.departments}"},{"type": "DatePicker","name": "date","label": "Preferred date","required": true},{"type": "Footer","label": "See free slots","on-click-action": {"name": "data_exchange","payload": {"department": "${form.department}","date": "${form.date}"}}}]}},{"id": "DETAILS","title": "Your details","terminal": true,"sensitive": ["phone"],"data": {"slots": {"type": "array","items": {"type": "object","properties": {"id": {"type": "string"},"title": {"type": "string"}}},"__example__": [{"id": "1030","title": "10:30 am"}]}},"layout": {"type": "SingleColumnLayout","children": [{"type": "RadioButtonsGroup","name": "slot","label": "Pick a time","required": true,"data-source": "${data.slots}"},{"type": "TextInput","name": "name","label": "Full name","required": true},{"type": "TextInput","name": "phone","label": "Phone","input-type": "phone","required": true},{"type": "Footer","label": "Confirm booking","on-click-action": {"name": "complete","payload": {"department": "${screen.APPOINTMENT.form.department}","date": "${screen.APPOINTMENT.form.date}","slot": "${form.slot}","name": "${form.name}","phone": "${form.phone}"}}}]}}]}
Two screens, one endpoint call, one completion payload. Use the newest version Meta lists as supported when you build yours.
How does Flow JSON work?
Choose a version and declare capabilities
Set
versionto a schema version Meta lists as supported for publishing — 7.3 is the newest in Meta's changelog at the time of writing, and versions up to 5.0 can no longer be published. Adddata_api_versionandrouting_modelonly if the Flow uses an endpoint.Lay out the screens
Give each screen an upper-case
idthat says what it is for (APPOINTMENT,DETAILS), a shorttitleand aSingleColumnLayoutof components. Mark the last screenterminal: trueand give it a Footer.Declare dynamic data
List every field a screen receives in its
datablock as a JSON Schema type with an__example__, then bind components to it with${data.field}.Wire the actions
Use
navigatefor moves that need no server,data_exchangewhen your endpoint must compute the next screen, andcompleteon the terminal Footer with exactly the payload you want at the webhook.Upload and validate
Upload the file as a
FLOW_JSONasset withPOST /{FLOW_ID}/assets. The response listsvalidation_errors, and a Flow with errors cannot be published.
Example: A two-screen booking Flow in Chennai
A Chennai dental clinic's booking Flow has two screens. APPOINTMENT shows a Dropdown of departments bound to ${data.departments} and a DatePicker; its Footer runs data_exchange so the clinic's endpoint can return only the free slots for that date. DETAILS is terminal: it lists the slots as radio buttons, asks for name and phone — with phone marked sensitive, so the completion summary masks it — and its Footer runs complete with the form values and ${screen.APPOINTMENT.form.department}. The routing_model is just {"APPOINTMENT": ["DETAILS"], "DETAILS": []}. The whole file is under 4 KB, lives in the clinic's Git repository and is uploaded by its deploy pipeline, which fails the build if validation_errors is not empty.
Common wrong turns
Common mistakes with Flow JSON
Using data_exchange without an endpoint
data_exchangeonly works when the Flow hasdata_api_version, arouting_modeland anendpoint_uri. For a static form, usenavigatebetween screens andcompleteat the end.Forgetting __example__
Every field declared in a screen's
datablock needs an example value. Without it validation fails and the Flow cannot be published.Heavy completion payloads
The
completepayload reaches your webhook inside the flow response message. Send only the fields you will store, and never base64 images.Assuming an edit reaches every customer
Uploading new Flow JSON to a published Flow returns it to Draft, and only messages sent after you publish again use the new file; Flows already delivered keep the old one. Make the webhook accept both payload shapes for a while, keep every version in Git, and clone instead when the live version must keep running untouched.
Routes that don't match the buttons
If a
navigateaction or an endpoint response targets a screen therouting_modeldoes not allow, the transition fails on the device and counts towards client error alerts. Generate both from one source.
Why does Flow JSON matter?
Every behaviour of a Flow — which questions are asked, in what order, what is checked on the device and what goes to your server — lives in this one file. Teams that treat it as code, with version control, review and automated validation, ship Flows that work first time. Teams that hand-edit it under pressure meet validation errors that block publishing, broken screen transitions that count against client error rates, and edits to a live Flow that never reach the customers who already received the old version.
Good practice
Best practices for Flow JSON
- Keep Flow JSON in Git and upload it from CI, failing the build on any
validation_errors. - Name screens after their purpose; the ids appear in endpoint requests and logs.
- Mark personal fields with
sensitive(version 5.1 and later) so the completion summary masks them. - Prefer
navigatewhen no server data is needed — everydata_exchangeadds latency and a failure point.