Flows
Build interactive WhatsApp Flows: forms, surveys, and booking experiences
Flows
WhatsApp Flows let you build native interactive forms, surveys, and booking experiences inside WhatsApp. Flows are created in DRAFT status, populated with a Flow JSON definition, then published for sending. Published flows are immutable -- to update, create a new flow (optionally cloning the old one).
Create and Publish a Flow
// Step 1: Create a flow
const { data } = await zernio.whatsappflows.createWhatsAppFlow({
body: {
accountId: 'YOUR_WHATSAPP_ACCOUNT_ID',
name: 'lead_capture_form',
categories: ['LEAD_GENERATION']
}
});
const flowId = data.flow.id;
// Step 2: Upload the Flow JSON
await zernio.whatsappflows.uploadWhatsAppFlowJson({
path: { flowId },
body: {
accountId: 'YOUR_WHATSAPP_ACCOUNT_ID',
flow_json: {
version: '6.0',
screens: [{
id: 'LEAD_FORM',
title: 'Get a Quote',
terminal: true,
success: true,
layout: {
type: 'SingleColumnLayout',
children: [
{ type: 'TextInput', name: 'full_name', label: 'Full Name', required: true, 'input-type': 'text' },
{ type: 'TextInput', name: 'email', label: 'Email', required: true, 'input-type': 'email' },
{ type: 'Footer', label: 'Submit', 'on-click-action': { name: 'complete', payload: { full_name: '${form.full_name}', email: '${form.email}' } } }
]
}
}]
}
}
});
// Step 3: Publish (irreversible)
await zernio.whatsappflows.publishWhatsAppFlow({
path: { flowId },
body: { accountId: 'YOUR_WHATSAPP_ACCOUNT_ID' }
});# Step 1: Create a flow
response = client.whatsapp_flows.create_whats_app_flow(
account_id='YOUR_WHATSAPP_ACCOUNT_ID',
name='lead_capture_form',
categories=['LEAD_GENERATION']
)
flow_id = response.flow.id
# Step 2: Upload the Flow JSON
client.whatsapp_flows.upload_whats_app_flow_json(
flow_id=flow_id,
account_id='YOUR_WHATSAPP_ACCOUNT_ID',
flow_json={
'version': '6.0',
'screens': [{
'id': 'LEAD_FORM',
'title': 'Get a Quote',
'terminal': True,
'success': True,
'layout': {
'type': 'SingleColumnLayout',
'children': [
{'type': 'TextInput', 'name': 'full_name', 'label': 'Full Name', 'required': True, 'input-type': 'text'},
{'type': 'TextInput', 'name': 'email', 'label': 'Email', 'required': True, 'input-type': 'email'},
{'type': 'Footer', 'label': 'Submit', 'on-click-action': {'name': 'complete', 'payload': {'full_name': '${form.full_name}', 'email': '${form.email}'}}}
]
}
}]
}
)
# Step 3: Publish (irreversible)
client.whatsapp_flows.publish_whats_app_flow(
flow_id=flow_id,
account_id='YOUR_WHATSAPP_ACCOUNT_ID'
)# Step 1: Create a flow
FLOW_ID=$(curl -s -X POST https://zernio.com/api/v1/whatsapp/flows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "YOUR_WHATSAPP_ACCOUNT_ID",
"name": "lead_capture_form",
"categories": ["LEAD_GENERATION"]
}' | jq -r '.flow.id')
# Step 2: Upload the Flow JSON
curl -X PUT "https://zernio.com/api/v1/whatsapp/flows/$FLOW_ID/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "YOUR_WHATSAPP_ACCOUNT_ID",
"flow_json": {
"version": "6.0",
"screens": [{
"id": "LEAD_FORM",
"title": "Get a Quote",
"terminal": true,
"success": true,
"layout": {
"type": "SingleColumnLayout",
"children": [
{"type": "TextInput", "name": "full_name", "label": "Full Name", "required": true, "input-type": "text"},
{"type": "TextInput", "name": "email", "label": "Email", "required": true, "input-type": "email"},
{"type": "Footer", "label": "Submit", "on-click-action": {"name": "complete", "payload": {"full_name": "${form.full_name}", "email": "${form.email}"}}}
]
}
}]
}
}'
# Step 3: Publish (irreversible)
curl -X POST "https://zernio.com/api/v1/whatsapp/flows/$FLOW_ID/publish" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"accountId": "YOUR_WHATSAPP_ACCOUNT_ID"}'Preview a Flow
Get a public web-preview URL that renders the flow (drafts included) so you can visualize it before publishing. The link works without login and can be embedded as an iframe or shared with stakeholders. It is reused across calls (valid ~30 days); pass invalidate: true to mint a fresh one (the previous link stops working).
const { data } = await zernio.whatsappflows.getWhatsAppFlowPreview({
path: { flowId },
query: { accountId: 'YOUR_WHATSAPP_ACCOUNT_ID' }
});
console.log(data.preview_url); // embed as <iframe src="...&interactive=true" />response = client.whatsapp_flows.get_whats_app_flow_preview(
flow_id=flow_id,
account_id='YOUR_WHATSAPP_ACCOUNT_ID'
)
print(response.preview_url)curl "https://zernio.com/api/v1/whatsapp/flows/$FLOW_ID/preview?accountId=YOUR_WHATSAPP_ACCOUNT_ID" \
-H "Authorization: Bearer YOUR_API_KEY"Version a Flow
A published flow is immutable, so the way to iterate is to clone it into a new draft. There are two distinct intents:
- New version -- a clone that stays in the original flow's version lineage, auto-numbered. Pass
cloneFlowIdplusasVersion: true. The list and create responses carryversion+lineageId, and the full chain is available via the versions endpoint. - Clone -- an independent duplicate with its own fresh lineage (version 1). Pass
cloneFlowIdonly.
// New version of a published flow (stays in the same lineage)
const { data } = await zernio.whatsappflows.createWhatsAppFlow({
body: {
accountId: 'YOUR_WHATSAPP_ACCOUNT_ID',
name: 'lead_capture_form',
categories: ['LEAD_GENERATION'],
cloneFlowId: 'PUBLISHED_FLOW_ID',
asVersion: true
}
});
console.log(`Created v${data.flow.version} (draft)`);
// Inspect the full version history (newest first)
const { data: history } = await zernio.whatsappflows.listWhatsAppFlowVersions({
path: { flowId: data.flow.id },
query: { accountId: 'YOUR_WHATSAPP_ACCOUNT_ID' }
});
history.versions.forEach(v => console.log(`v${v.version} - ${v.status}`));# New version of a published flow (stays in the same lineage)
response = client.whatsapp_flows.create_whats_app_flow(
account_id='YOUR_WHATSAPP_ACCOUNT_ID',
name='lead_capture_form',
categories=['LEAD_GENERATION'],
clone_flow_id='PUBLISHED_FLOW_ID',
as_version=True
)
print(f"Created v{response.flow.version} (draft)")
# Inspect the full version history (newest first)
history = client.whatsapp_flows.list_whats_app_flow_versions(
flow_id=response.flow.id,
account_id='YOUR_WHATSAPP_ACCOUNT_ID'
)
for v in history.versions:
print(f"v{v.version} - {v.status}")# New version of a published flow (stays in the same lineage)
curl -X POST https://zernio.com/api/v1/whatsapp/flows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "YOUR_WHATSAPP_ACCOUNT_ID",
"name": "lead_capture_form",
"categories": ["LEAD_GENERATION"],
"cloneFlowId": "PUBLISHED_FLOW_ID",
"asVersion": true
}'
# Version history (newest first)
curl "https://zernio.com/api/v1/whatsapp/flows/$FLOW_ID/versions?accountId=YOUR_WHATSAPP_ACCOUNT_ID" \
-H "Authorization: Bearer YOUR_API_KEY"Version history is tracked by Zernio (Meta has no native flow versioning). Omit asVersion to get an independent clone instead of a new version. A flow that was never cloned reports as version: 1 of its own lineage.
Read Flow Responses
When a customer completes a flow, their submission arrives as an nfm_reply webhook. List those submissions per flow without touching the inbox:
const { data } = await zernio.whatsappflows.listWhatsAppFlowResponses({
query: { accountId: 'YOUR_WHATSAPP_ACCOUNT_ID', flowId: 'YOUR_FLOW_ID' }
});
data.responses.forEach(r => {
console.log(`${r.from} @ ${r.receivedAt}:`, r.data); // r.data = submitted fields
});response = client.whatsapp_flows.list_whats_app_flow_responses(
account_id='YOUR_WHATSAPP_ACCOUNT_ID',
flow_id='YOUR_FLOW_ID'
)
for r in response.responses:
print(f"{r.from_} @ {r.received_at}:", r.data) # r.data = submitted fieldscurl "https://zernio.com/api/v1/whatsapp/flow-responses?accountId=YOUR_WHATSAPP_ACCOUNT_ID&flowId=YOUR_FLOW_ID" \
-H "Authorization: Bearer YOUR_API_KEY"Per-flow scoping works because Zernio encodes the flow id into the flow_token it auto-generates at send time (<flowId>:<uuid>), and that token round-trips through Meta inside the response. If you send a flow with your own custom flow_token, those responses won't be attributed to a flow (they still arrive via webhook). Only flows sent after this token convention shipped are attributable.
Flow Lifecycle
| Status | Description |
|---|---|
DRAFT | Editable. Upload/update JSON, change name and categories. |
PUBLISHED | Immutable. Can be sent to users. To update, create a new version via cloneFlowId + asVersion. |
DEPRECATED | No longer sendable. |
BLOCKED | Blocked by Meta for policy violations. |
THROTTLED | Temporarily rate-limited by Meta. |
Flow Categories
SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER
Publishing a flow is irreversible. Once published, the flow and its JSON cannot be modified. To make changes, create a new version (cloneFlowId + asVersion: true) which copies the published flow into a fresh draft you can edit and publish.
See WhatsApp Flows API Reference for all endpoints.