Skip to content

Concept map· 6 connected terms

Flow JSON

Also called: WhatsApp Flow JSON · Flows JSON schema · Flow definition

In one line
Flow JSON is the versioned document that defines a WhatsApp Flow: its screens, components, routing between screens and actions such as navigate and complete.

Reviewed

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 version and screens; Flows with an endpoint add data_api_version (4.0 is recommended) and a routing_model.
  • Each screen has an id, a SingleColumnLayout of components, and a terminal screen must include a Footer.
  • Actions move the customer: navigate to a screen, data_exchange to your endpoint, complete to 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

9 / 9
{
"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}"
}
}
}
]
}
}
]
}
A booking Flow in the shape WhatsApp expects. Step through the fields that matter: the version, the routing model, a screen's dynamic data, a component's binding and the actions that move between screens.

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?

  1. Choose a version and declare capabilities

    Set version to 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. Add data_api_version and routing_model only if the Flow uses an endpoint.

  2. Lay out the screens

    Give each screen an upper-case id that says what it is for (APPOINTMENT, DETAILS), a short title and a SingleColumnLayout of components. Mark the last screen terminal: true and give it a Footer.

  3. Declare dynamic data

    List every field a screen receives in its data block as a JSON Schema type with an __example__, then bind components to it with ${data.field}.

  4. Wire the actions

    Use navigate for moves that need no server, data_exchange when your endpoint must compute the next screen, and complete on the terminal Footer with exactly the payload you want at the webhook.

  5. Upload and validate

    Upload the file as a FLOW_JSON asset with POST /{FLOW_ID}/assets. The response lists validation_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_exchange only works when the Flow has data_api_version, a routing_model and an endpoint_uri. For a static form, use navigate between screens and complete at the end.

  • Forgetting __example__

    Every field declared in a screen's data block needs an example value. Without it validation fails and the Flow cannot be published.

  • Heavy completion payloads

    The complete payload 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 navigate action or an endpoint response targets a screen the routing_model does 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 navigate when no server data is needed — every data_exchange adds latency and a failure point.

Flow JSON FAQs

Where do I write Flow JSON?+
In Meta's Flow Builder in WhatsApp Manager, or in any editor and then uploaded through the Flows API. The Builder shows a live preview; the API route (`POST /{FLOW_ID}/assets`) suits teams that keep Flow JSON in version control and deploy it from CI.
Which Flow JSON version should I use?+
Use the newest version Meta lists as supported for publishing, unless you depend on behaviour that changed. Newer versions add components and features such as cross-screen references (4.0) and `sensitive` fields (5.1); older versions may remain valid only for sending existing Flows.
What is the difference between Flow JSON and a Flow endpoint?+
Flow JSON is the static definition of screens and actions; the endpoint is a server that returns dynamic data while the Flow runs. Every Flow has Flow JSON, but only Flows whose screens depend on live data need an endpoint.
What is routing_model in Flow JSON?+
It maps each screen id to the screens it can move to next, and it is required when the Flow uses an endpoint. You list forward moves only — back navigation is automatic — with at most 10 branches, and every route must end at a terminal screen.

See InfiQ pricing

Try it on the official WhatsApp Business API

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.