WhatsApp Flows for developers

By Andrés Matte

A developer guide to WhatsApp Flows: static vs dynamic flows, Flow JSON, data endpoints, encryption, response contracts, and how Kapso reduces the operational work.

WhatsApp Flows let you put a structured form inside WhatsApp.

A user can book an appointment, pick a delivery slot, complete a lead form, submit support details, or answer a multi-step workflow without leaving the chat.

With Kapso, the practical model is simpler than the raw Meta setup. You mainly care about two things:

  1. The Flow JSON that defines the screens, fields, navigation, and data schema.
  2. The Kapso Function that handles dynamic Flow requests and returns the next screen payload.

Kapso handles the operational layer around those two artifacts: flow registration, versions, publishing, encryption/decryption, endpoint URLs, data endpoint secrets, signature verification, preview links, and invocation logs.

If your coding agent has the Kapso agent skills installed, it can do much of the framework work for you:

npm install -g @kapso/cli
npx skills add gokapso/agent-skills
kapso setup

Then the agent can read the Flow JSON rules, create or update the Flow, write the data endpoint Function, deploy it, register the endpoint, and inspect invocation logs. You still need to describe the product flow clearly, but you do not need to learn every Flow JSON edge case before starting.

What a WhatsApp Flow is

A Flow is a native WhatsApp interaction defined by Flow JSON. The JSON declares screens, components, form fields, navigation, data dependencies, and how the Flow completes.

The user opens the Flow from a WhatsApp message button. WhatsApp renders the UI. When the user moves through screens or submits data, the Flow either uses static JSON or calls a data endpoint.

A small dynamic Flow JSON skeleton looks like this:

{
  "version": "7.3",
  "data_api_version": "3.0",
  "routing_model": {
    "SELECT_DATE": ["SELECT_SLOT"],
    "SELECT_SLOT": []
  },
  "screens": [
    {
      "id": "SELECT_DATE",
      "title": "Book appointment",
      "layout": {
        "type": "SingleColumnLayout",
        "children": [
          {
            "type": "DatePicker",
            "name": "date",
            "label": "Date",
            "on-select-action": {
              "name": "data_exchange",
              "payload": { "date": "${form.date}" }
            }
          }
        ]
      }
    },
    {
      "id": "SELECT_SLOT",
      "title": "Select time",
      "data": {
        "available_slots": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "title": { "type": "string" }
            }
          },
          "__example__": [{ "id": "09:00", "title": "9:00 AM" }]
        }
      },
      "terminal": true,
      "layout": {
        "type": "SingleColumnLayout",
        "children": [
          {
            "type": "RadioButtonsGroup",
            "name": "slot",
            "label": "Time slot",
            "data-source": "${data.available_slots}"
          }
        ]
      }
    }
  ]
}

The exact version and component rules should come from the current Meta and Kapso docs. The shape is the point: screens declare what data they expect, and the endpoint supplies that data at runtime.

Static vs dynamic flows

There are two practical modes.

Static flows do not call your backend while the user moves through the Flow. The screens and options are defined ahead of time. Use these for simple forms, fixed menus, or flows where all choices are known before sending.

Dynamic flows use a data endpoint. WhatsApp calls the endpoint during the Flow so your backend can return screen data, validate input, branch, or complete the flow.

Use dynamic flows when:

  • Available options depend on the user.
  • Inventory, appointment slots, or account state changes at runtime.
  • You need server-side validation.
  • You need to persist variables or correlate the Flow with your app state.

The data endpoint loop

For dynamic flows on Kapso, the loop is:

  1. The user opens the Flow in WhatsApp.
  2. Meta sends an encrypted request.
  3. Kapso decrypts it and forwards a JSON payload to your Function.
  4. Your Function returns the next screen and data.
  5. Kapso encrypts the response and returns it to Meta.
  6. WhatsApp renders the next state.

The request includes the current action, screen, submitted data, and flow_token. The common actions you should design around are INIT, data_exchange, and BACK; follow the current Kapso and Meta docs for any back-navigation behavior because it depends on how the Flow is wired.

The response contract

The response must be exact.

Meta’s data endpoint contract requires this shape, and Kapso forwards/validates that contract for dynamic Flows:

{
  "version": "3.0",
  "screen": "NEXT_SCREEN_ID",
  "data": {}
}

The most common bug is returning HTTP 200 with a malformed Flow payload. If version is missing, screen does not exist, or data does not match the screen schema, the user experience can fail even though your server logs show success.

For a completion response, return the success screen and include any extension response parameters your product needs.

Validation errors should also return a valid Flow response. Stay on the current screen, include the data the screen still needs, and add an error field the screen knows how to render.

{
  "version": "3.0",
  "screen": "APPOINTMENT_SLOTS",
  "data": {
    "error_message": "That time is no longer available.",
    "available_slots": [
      { "id": "slot-2", "title": "Today 11:00" }
    ]
  }
}

Flow JSON rules developers trip over

A few rules are worth memorizing.

Meta/Flow JSON rules:

  • Use the current Flow JSON version required by Meta and supported by Kapso. Check the current docs before editing a published guide.
  • Dynamic flows use data_api_version: "3.0".
  • Dynamic flows need a routing model.
  • Declare every dynamic field the endpoint may return under the screen data schema.
  • Dynamic references must follow Meta’s expression rules. For example, ${form.*} and ${data.*} generally need to be the full property value unless you use the supported expression syntax.

Kapso-specific rules:

  • Kapso can inject the data endpoint for you, so do not include endpoint_uri or data-channel fields when using Kapso-managed endpoints.
  • Kapso handles encryption and decryption before invoking your Function.
  • Keep endpoint work well under the response timeout. Kapso’s internal guidance is to keep total endpoint work under about 12 seconds.
  • Use invocation logs to debug request and response shape, not only HTTP status.

What to model in your app

For a production Flow system, model:

Field Why it matters
Flow ID The Kapso or internal flow record.
Meta flow ID The upstream Flow object in Meta.
Published version Published flows are not the same as editable drafts.
Phone number Publishing and sending need a production WhatsApp number.
Flow token Correlates a user session with your app state.
Responses The final submitted data and intermediate state if needed.
Endpoint invocations Debugging dynamic flows requires request and response logs.

Without invocation logs, dynamic flows become hard to debug because the visible failure may happen inside WhatsApp, not in your UI.

The flow_token should be generated by your backend when you send the Flow message. Treat it as an opaque session correlator: a UUID is fine for simple systems, while HMAC-signed or database-backed tokens are safer when the completion has business consequences. On completion, use the token to reconcile the submitted data with the record that launched the Flow.

Publishing is also part of the model. Draft flows are editable; published flows are what users can receive. Treat a published Flow like a versioned contract because changing a live form can affect messages already sent to users.

To launch the Flow, send an interactive Flow message from the connected phone number:

await client.messages.sendInteractiveFlow({
  phoneNumberId: "1234567890",
  to: "15551234567",
  bodyText: "Book your appointment",
  parameters: {
    flowId: "1197715005513101",
    flowCta: "Open",
    flowMessageVersion: "3",
    flowAction: "navigate",
    flowActionPayload: { screen: "SELECT_DATE" }
  }
});

When not to use a Flow

Do not use a Flow for every interaction. Use a Flow when structure improves completion: appointments, checkout, qualification, onboarding, support intake, consent, preference collection, or guided forms.

Use normal messages for simple questions, free-form conversations, tiny choice sets, or cases where a link to your app is clearer.

How Kapso fits

Kapso handles the operational parts around WhatsApp Flows:

  • Flow management: create, update, publish, test, and sync with Meta.
  • Dynamic endpoints: encryption/decryption, endpoint URLs, secret management, and invocation logging.
  • Production context: connected numbers, Workflows, Functions, previews, and the send surface for launching the Flow.
  • Agent workflow: with @kapso/cli and gokapso/agent-skills, an AI coding agent can generate the Flow JSON and Function, deploy the endpoint, and debug logs.

You still own the product logic: what information to collect, what validations matter, what state to persist, and what should happen after the Flow completes.