WhatsApp

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.

flows/appointment.json
{
  "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.
  • version is a Flow JSON version Meta publishes.
  • screens is a non-empty array whose ids are letters and underscores, distinct, and not SUCCESS, which Meta reserves.
  • A routing_model, or a data_exchange action, makes it a data-exchange flow (see WhatsApp flows with your own endpoint); it names no data_api_version or data_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_BOOKING

The 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_3xk9mvt4znw8rh7q

Recording 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_3xk9mvt4znw8rh7q
curl -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:

StatusMeaningSends
draftCreated on Meta but not published; validationErrors says why.No
publishedLive. Customers can open it.Yes
throttledMeta limits how many messages it sends an hour.Yes
blockedMeta blocked it. It no longer sends.No
deprecatedRetired 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
FieldMeaning
flowIdThe Samva flow to open.
ctaThe button label, up to 30 characters.
bodyThe message text, as for reply buttons.
header, footerOptional. The header is text only.
tokenOptional, up to 1,024 characters. Returned unchanged on the answer; use it to tie the answer to a record.
actionOptional. navigate opens on screen; data_exchange asks your endpoint for the first screen.
screenOptional. The screen to open on, instead of the first one. It must be a screen of the version.
dataOptional. 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

On this page