WhatsApp flows
Author a WhatsApp flow as a project file, publish it as a version, register it with Meta, send it in the service window, and read the customer's answers.
A flow is a form that opens inside the WhatsApp chat: the customer taps a button on your message,
fills in one or more screens, and submits. You get the answers back as a message.received webhook.
Use flows for bookings, sign-ups, surveys, and lead capture, where a reply button or a list is too
small and a link would take the customer out of WhatsApp.
You author a flow as a file in a template project, Meta's Flow JSON. You record the file at a commit as a version, register the version with Meta on a connected number's business account, and send it as an interactive message. A flow message is free-form content, so it needs an open 24-hour service window.
Write the flow file
A flow lives at flows/<handle>.json in a template project, where <handle> is lowercase
kebab-case and unique in your organization. This file has two screens: the customer fills in a form,
then confirms. The second screen is terminal, and its complete action ends the flow and sends the
payload back to you.
{
"version": "7.3",
"screens": [
{
"id": "DETAILS",
"title": "Book a visit",
"layout": {
"type": "SingleColumnLayout",
"children": [
{
"type": "Form",
"name": "form",
"children": [
{
"type": "TextInput",
"name": "full_name",
"label": "Full name",
"input-type": "text",
"required": true
},
{
"type": "Dropdown",
"name": "service",
"label": "Service",
"required": true,
"data-source": [
{ "id": "cut", "title": "Haircut" },
{ "id": "color", "title": "Colour" }
]
},
{
"type": "Footer",
"label": "Continue",
"on-click-action": {
"name": "navigate",
"next": { "type": "screen", "name": "CONFIRM" },
"payload": {
"full_name": "${form.full_name}",
"service": "${form.service}"
}
}
}
]
}
]
}
},
{
"id": "CONFIRM",
"title": "Confirm",
"terminal": true,
"success": true,
"data": {
"full_name": { "type": "string", "__example__": "Ada Lovelace" },
"service": { "type": "string", "__example__": "cut" }
},
"layout": {
"type": "SingleColumnLayout",
"children": [
{ "type": "TextHeading", "text": "Check your booking" },
{ "type": "TextBody", "text": "${data.full_name}: ${data.service}" },
{
"type": "Footer",
"label": "Book",
"on-click-action": {
"name": "complete",
"payload": {
"full_name": "${data.full_name}",
"service": "${data.service}"
}
}
}
]
}
}
]
}Samva checks the file when you record a version, before Meta sees it:
- It is a JSON object of at most 1 MB.
versionis a Flow JSON version Meta publishes.screensis a non-empty array whose ids are letters and underscores, distinct, and notSUCCESS, which Meta reserves.- A
routing_model, or adata_exchangeaction, makes it a data-exchange flow (see WhatsApp flows with your own endpoint); it names nodata_api_versionordata_channel_uri.
Meta's own validator runs when you register the version, and its findings are the final word.
Create, publish, and register
Create the flow once, naming the template project that holds the file and at least one of Meta's
categories (SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US,
CUSTOMER_SUPPORT, SURVEY, OTHER):
samva whatsapp flows create --project tproj_k9mvt4znw8rh7q2x \
--handle appointment --category APPOINTMENT_BOOKINGThe response carries the flow id (waflow_…) that sends and later commands use. Commit
flows/appointment.json to the project and push it, then publish the flow: this records the
file at that commit as the flow's next version. Run it inside a clone of the project:
samva whatsapp flows publish waflow_3xk9mvt4znw8rh7qRecording the same file content again returns the version that already holds it; a changed file is
the next version. Over the REST API, POST /v1/whatsapp/flows/{id}/versions takes the commitSha
on the project's default branch and the project workspace's expectedWorkspaceRevision.
Register the version to create it on Meta and publish it there. It registers on the business account of your default number; pass a business account id to use another connected number's account.
samva whatsapp flows register waflow_3xk9mvt4znw8rh7qcurl -X POST https://api.samva.dev/v1/whatsapp/flows/waflow_3xk9mvt4znw8rh7q/registrations \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{}'The response is the registration:
{
"id": "wafreg_7q2xk9mvt4znw8rh",
"flowId": "waflow_3xk9mvt4znw8rh7q",
"versionId": "wafver_k9mvt4znw8rh7q2x",
"versionNumber": 1,
"wabaId": "102290129340398",
"providerFlowId": "1689556908129832",
"name": "appointment v1",
"status": "published",
"validationErrors": [],
"publishedAt": "2026-10-17T10:30:00.000Z",
"statusChangedAt": "2026-10-17T10:30:00.000Z",
"createdAt": "2026-10-17T10:30:00.000Z",
"updatedAt": "2026-10-17T10:30:00.000Z"
}If Meta's validator rejects the flow, the call answers a validation error that lists each finding
with its path in the file. The registration stays a draft: fix the file, commit it, publish a new
version, and register again. Registering a version that is already published returns its
registration, so a retry is safe.
List a flow's registrations with samva whatsapp flows registrations <id> or
GET /v1/whatsapp/flows/{id}/registrations, and its versions with
GET /v1/whatsapp/flows/{id}/versions.
Registration status
The registration's status is Meta's, and it follows Meta's decisions after you register:
| Status | Meaning | Sends |
|---|---|---|
draft | Created on Meta but not published; validationErrors says why. | No |
published | Live. Customers can open it. | Yes |
throttled | Meta limits how many messages it sends an hour. | Yes |
blocked | Meta blocked it. It no longer sends. | No |
deprecated | Retired on Meta. It no longer sends. | No |
Each change sends a flow.registration.updated webhook event with the registration and the status
it moved from. The event catalog describes delivery and signing.
Versions
Meta never changes a flow after it is published, so every version of your flow is its own flow on
Meta, named <handle> v<number> (appointment v2). Registering a new version does not touch the
old one:
- A message you already sent keeps opening the version it was sent with, so a customer who taps an older message still gets a working flow.
- A new send uses the newest published version registered on the sending number's business account. Register a version before you send it.
- Samva never deprecates an older version for you. Retire one in WhatsApp Manager when no message that carries it is still worth opening.
If a business account already has a published flow with the same name that Samva did not register, registration is refused with a conflict that names it. Rename or deprecate that flow in WhatsApp Manager, then register again.
Send a flow
A flow message is an interactive message of type flow. Name the flow by its id, the text of the
message, and the label of the button that opens it:
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": {
"interactive": {
"type": "flow",
"body": "Book your visit in a few taps.",
"flowId": "waflow_3xk9mvt4znw8rh7q",
"cta": "Book now",
"token": "booking-4821"
}
}
}'samva whatsapp send --to +16175559876 \
--flow waflow_3xk9mvt4znw8rh7q --cta "Book now" \
--body "Book your visit in a few taps." --flow-token booking-4821| Field | Meaning |
|---|---|
flowId | The Samva flow to open. |
cta | The button label, up to 30 characters. |
body | The message text, as for reply buttons. |
header, footer | Optional. The header is text only. |
token | Optional, up to 1,024 characters. Returned unchanged on the answer; use it to tie the answer to a record. |
action | Optional. navigate opens on screen; data_exchange asks your endpoint for the first screen. |
screen | Optional. The screen to open on, instead of the first one. It must be a screen of the version. |
data | Optional. An object the opening screen reads as ${data.<key>}. It needs screen. |
The send is refused with a validation error when the flow has no published or throttled
registration on the sending number's business account, when screen is not a screen of the version
that would open, or when the service window is closed.
Open a flow from a template
A flow message needs an open service window. To open a flow outside it, give a
message template a
WhatsApp.FlowButton that names the flow's handle:
<WhatsApp.Buttons>
<WhatsApp.FlowButton flow="appointment">{"Book now"}</WhatsApp.FlowButton>
</WhatsApp.Buttons>Register the flow on the business account first, then submit the template. Samva gives Meta the
flow's id on that account, so a template submission is refused while the flow has no published or
throttled registration there. The template keeps opening the version it was submitted with: after
you register a newer version, submit the template again to move it.
Pass flowToken when you send the template (--flow-token on samva whatsapp send --template);
it comes back as flowToken exactly as a flow message's token does. A
broadcast or a journey's WhatsApp step sends one message per
contact, and each message's flow button carries that message's id.
Receive the answer
When the customer submits the flow, Samva publishes a message.received webhook with
channel: "whatsapp" and a whatsapp.flowResponse object. response is the payload your flow's
complete action sent, and flowToken is the token your message carried. A send without a
token carries the message's id, so flowToken names the message, and through it the contact,
either way.
{
"type": "message.received",
"timestamp": "2026-10-17T10:36:12.400Z",
"data": {
"messageId": "msg_01j8k4n6p8q2r4s6t9vw",
"channel": "whatsapp",
"status": "delivered",
"direction": "inbound",
"from": "+16175559876",
"to": "+16175550123",
"messageType": "interactive",
"providerMessageId": "wamid.HBgLMTYxNzU1NTk4NzYVAgASGBQz",
"whatsapp": {
"flowResponse": {
"flowToken": "booking-4821",
"response": { "full_name": "Ada Lovelace", "service": "cut" },
"body": "Sent"
},
"context": {
"messageId": "msg_01j8k3m5n7p9q2r4s6tv",
"providerMessageId": "wamid.HBgLMTYxNzU1NTAxMjMVAgARGBI2"
}
}
}
}whatsapp.context.messageId is the id the flow message's send returned, as for any
reply.
body is what WhatsApp showed in the chat after the submit. Read response by the keys your own
complete payload defines, and ignore any you do not recognize.
Limits and what is not supported
- Endpoints. A flow that asks your server for its screens needs an endpoint and a secret; see WhatsApp flows with your own endpoint.
- One flow per template. A template carries at most one flow button.
- Free-form only. A flow message needs an open service window; outside it, send a template first.
- Flow JSON limits are Meta's, plus the 1 MB file size above. Meta's validator reports the rest when you register.
Related documentation
Interactive and media messages
Send media, reply buttons, lists, links, reactions, locations, and contacts on WhatsApp, and receive the customer's replies.
WhatsApp flows with your own endpoint
Let a WhatsApp flow ask your server for each screen, verify Samva's signed request, answer with the next screen, and watch the flow's health.