/flows), through the REST API (/v1/flows), or attach one to a template with a FLOW button.
Flows exist only on Meta (Cloud API) numbers. They belong to the WhatsApp Business Account (WABA), not to a single number, so every number of that WABA can send the same Flows.
Two kinds of Flow
The difference is whether your backend is involved while the form is open.
A
NAVIGATE Flow is delivered, filled in, and submitted once. You receive the whole answer at the end. Most Flows are this kind, and they need nothing beyond a Flow JSON.
A data_exchange Flow talks back mid-form: the customer picks a date and the next screen shows the slots that are actually free. That requires an HTTPS endpoint that Meta calls per screen — and Meta encrypts every one of those calls.
Lifecycle
1
Create
A Flow starts in
DRAFT. Give it a name and one or more categories (SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER).2
Upload the Flow JSON
The screens themselves are a JSON document. Meta answers
200 with a list of validation errors rather than refusing — so a green response does not mean the document compiled. Always read validationErrors.3
Publish
Publishing makes the Flow sendable — and is irreversible on Meta’s side. A published Flow can never be edited again.
4
Clone to make a new version
Because publishing is final, the way to change a live Flow is to clone it. The clone is a new Flow with a new id, and Pilot Status records the lineage so you can follow the chain from one version to the next.
5
Deprecate
Retiring a Flow does not delete it, and does not delete the answers people already submitted. You can name the Flow that replaces it, which is what keeps the version chain readable.
Sending a Flow
Attach the Flow to a template using a FLOW button. The button binds byflow_id, by flow_name, or by inline flow_json. Every send mints a fresh flow_token — a one-time value that ties that specific send to the answer that comes back later.
Receiving the answer
When the customer submits, Meta sends annfm_reply carrying the flow_token and the submitted form as a JSON string. Pilot Status stores it and exposes it at GET /v1/flows/{id}/responses, joined back to the contact who answered.
Responses are kept for a retention window and pruned afterwards. The window is enforced on read, so a response past its expiry is never served — even if the pruner has not reached it yet.
data_exchange: Pilot Status handles the encryption
This is the part that usually stops teams from shipping an interactive Flow.
Meta encrypts every endpoint call: an AES-GCM payload whose key arrives RSA-2048/OAEP-SHA-256-wrapped under your public key, and the reply must go back encrypted with the same AES key under a bitwise-inverted IV. Getting any of that subtly wrong produces a Flow that simply never advances, with no error on either side.
So Pilot Status is the endpoint. We hold the key, decrypt Meta’s request, and forward plain JSON over HTTPS to a webhook you control. Your reply is encrypted on the way back. You never implement RSA, AES-GCM, or the inverted IV.
data_exchange is not self-service yet. Everything described below is built and running, but the three setup steps have no public endpoint and no dashboard screen yet — talk to support to have a number set up. NAVIGATE Flows need none of this and are fully self-service today.1
Register a key for the number
Pilot Status generates an RSA-2048 keypair, stores the private half encrypted, and registers the public half with Meta for that phone number. You get back the endpoint URL to use.
2
Point the Flow at it
Set that URL as the Flow’s endpoint. It carries two segments — one identifying the number, one identifying the Flow — because Meta’s request does not say which Flow it belongs to.
3
Set your webhook URL on the Flow
This is where we forward the decrypted JSON. It is configured per Flow, because one number can serve several forms and each one is usually a different service on your side.
What your webhook receives
action is INIT when the form opens, BACK when the customer goes back, and data_exchange when they submit a screen.
The request is signed with an HMAC-SHA256 of the raw body in the x-pilot-status-signature header — the same header and the same scheme as Pilot Status outbound webhooks, so if you already verify those, you need no second code path.
What your webhook must return
Plain JSON, naming the next screen and the data it needs:SUCCESS screen:
Answer within a few seconds. Meta is holding the request open while a person watches a spinner, and there is no retry — a slow answer is a failed screen. Do the slow work after you reply, not before.
When your webhook fails
If your server is down, times out, or answers something unusable, Pilot Status does not pass the failure through as a broken screen. The customer sees a generic error on the screen they are already looking at, and can retry — nothing they typed is lost, and nothing about your infrastructure is shown to them.Health and status
Each Flow carries a status (DRAFT, PUBLISHED, DEPRECATED, BLOCKED, THROTTLED), the Flow JSON version Meta compiled, and a health verdict.
Meta discontinued the Flows Metrics API on 2026-04-30 with no successor. The
flows webhook is now the only channel that ever reports a Flow being throttled or a public key going stale — Pilot Status subscribes to it and records what it says on the Flow, so the signal survives past the log window.