Description
What is a Flow endpoint?
A Flow endpoint — also called the Flow data endpoint or data channel — is an HTTPS URL on your server that a WhatsApp Flow calls while the customer is using it. Flows that only collect answers and submit them at the end need no endpoint. Flows that must show live data (free appointment slots, a price quote, a stock check) or validate input before moving on use one: the Flow JSON declares data_api_version and a routing_model, and the URL is registered as the Flow's endpoint_uri.
Every call is a POST with three base64 fields: encrypted_aes_key, encrypted_flow_data and initial_vector. The AES key is a one-time 128-bit key encrypted with the RSA public key you uploaded for the phone number (RSA-OAEP with SHA-256); the flow data is encrypted with AES-128-GCM. Decrypted, the payload holds version (3.0), an action — INIT when the Flow opens, BACK when the customer returns to a screen with refresh_on_back, data_exchange when a screen submits, or ping for health checks — plus the current screen, the submitted data and the flow_token you set when sending the Flow. Flows on Flow JSON 7.3 or later with data_api_version 4.0 also send a flow_token_signature that proves the token came from a genuine Flow message.
Your answer is JSON naming the next screen and its data, or the reserved SUCCESS screen with extension_message_response.params (including the flow_token) to end the Flow. It must be encrypted with the same AES key and the initial vector with every bit inverted, then returned as a base64 string. The endpoint is not your messages webhook: it serves the Flow synchronously while it is open, whereas the final answers reach the webhook later as a flow response message.
Key takeaways
- A Flow endpoint is your HTTPS server that WhatsApp calls during a Flow to load screen data or validate input.
- Each request carries an RSA-OAEP-encrypted AES-128 key; the body is AES-GCM, and your reply reuses the key with a bit-flipped IV.
- Answer
pinghealth checks with statusactive, and return HTTP 421 when a request cannot be decrypted. - Requests time out after 10 seconds; Meta advises aiming for under 1 second, and p90 latency alerts start at 1 second.
- An unhealthy endpoint gets its Flow throttled to 10 sends an hour, then blocked for everyone.
Usage
A Flow endpoint in code
import crypto from "node:crypto";import express from "express";const app = express();app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));const PRIVATE_KEY = crypto.createPrivateKey({key: process.env.FLOW_PRIVATE_KEY, // PEM from your secrets managerpassphrase: process.env.FLOW_KEY_PASSPHRASE,});function decrypt({ encrypted_aes_key, encrypted_flow_data, initial_vector }) {const aesKey = crypto.privateDecrypt({ key: PRIVATE_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },Buffer.from(encrypted_aes_key, "base64"),);const data = Buffer.from(encrypted_flow_data, "base64");const iv = Buffer.from(initial_vector, "base64");const decipher = crypto.createDecipheriv("aes-128-gcm", aesKey, iv);decipher.setAuthTag(data.subarray(-16)); // last 16 bytes are the GCM tagconst plain = Buffer.concat([decipher.update(data.subarray(0, -16)), decipher.final()]);return { body: JSON.parse(plain.toString("utf8")), aesKey, iv };}function encrypt(response, aesKey, iv) {const flipped = Buffer.from(Array.from(iv, (b) => ~b & 0xff)); // invert every IV bitconst cipher = crypto.createCipheriv("aes-128-gcm", aesKey, flipped);return Buffer.concat([cipher.update(JSON.stringify(response), "utf8"),cipher.final(),cipher.getAuthTag(),]).toString("base64");}app.post("/flows/endpoint", async (req, res) => {if (!validSignature(req.rawBody, req.get("X-Hub-Signature-256"))) return res.sendStatus(432);let ctx;try { ctx = decrypt(req.body); } catch { return res.sendStatus(421); } // client re-fetches keyconst { action, screen, data, flow_token } = ctx.body;let reply;if (action === "ping") reply = { data: { status: "active" } };else if (data?.error) reply = { data: { acknowledged: true } }; // client error notificationelse if (!(await isLiveToken(flow_token))) return res.sendStatus(427);else reply = await nextScreen(action, screen, data, flow_token); // { screen, data }res.type("text/plain").send(encrypt(reply, ctx.aesKey, ctx.iv));});
Verify, decrypt, route on action, then encrypt the reply with the flipped IV.
Mechanics
How does a Flow endpoint work?
step 1 — Generate and upload a key pair
Create a 2048-bit RSA key pair, keep the private key in a secrets manager, and upload the public key with
POST /{PHONE_NUMBER_ID}/whatsapp_business_encryption. The call signs the key for that one number, so repeat it for every number that sends the Flow and confirmbusiness_public_key_signature_statusisVALIDwith a GET.step 2 — Register the endpoint
Set
endpoint_urion the Flow through the Flows API or Flow Builder, and declaredata_api_versionandrouting_modelin the Flow JSON. The URL must be public HTTPS with a valid certificate.step 3 — Verify and decrypt every request
Check the
X-Hub-Signature-256HMAC with your app secret (HTTP 432 on mismatch), RSA-decrypt the AES key, then AES-GCM-decrypt the body, whose last 16 bytes are the auth tag. If decryption fails, return 421 so the client re-fetches your public key and retries. With Flow JSON 7.3+ anddata_api_version4.0, the payload also carriesflow_token_signature, a JWT (HS256) signed with your app secret: verify it and check the token inside matchesflow_token.step 4 — Handle the action
Answer
pingwith{"data": {"status": "active"}}. ForINIT,BACKanddata_exchange, return the next screen and its data, or the same screen witherror_messageto show the customer a problem. Acknowledge client error notifications with{"data": {"acknowledged": true}}, and return 427 for aflow_tokenthat is no longer valid.step 5 — Encrypt and reply fast
Encrypt the JSON with the same key and the bit-inverted IV and send it as a base64 text body with status 200. Requests time out after 10 seconds; aim for under one second by caching reference data and avoiding slow third-party calls.
step 6 — Watch the health alerts
Subscribe to the
flowswebhook field and alarm onENDPOINT_ERROR_RATE,ENDPOINT_LATENCYandENDPOINT_AVAILABILITYevents — they are the early warning before a Flow is throttled.
Properties
A Flow endpoint: key facts and limits
- Request fields
- encrypted_aes_key, encrypted_flow_data, initial_vector (base64)
- Key exchange
- RSA-OAEP with SHA-256, 2048-bit key pair
- Body cipher
- AES-128-GCM, 16-byte tag; reply uses the bit-inverted IV
- Actions
- INIT, BACK, data_exchange, ping
- Timeout
- 10 seconds; Meta advises aiming for under 1 second
- Your error codes
- 421 cannot decrypt · 432 bad signature · 427 expired flow_token
- Health alerts
- p90 latency 1 / 5 / 7 s; error rate 5 / 10 / 50%; availability below 90%
- Unhealthy Flow
- Throttled to 10 messages an hour, then Blocked
Caveats
Common mistakes with a Flow endpoint
Returning plain JSON
Every screen response — including one carrying
error_message— must be encrypted and base64-encoded. A plain JSON body with status 200 makes the customer's app show a generic error.Reusing the request IV
The reply must use the request's initial vector with every bit flipped. Reusing the same IV fails on the client and, with GCM, undermines the cipher's security.
Slow dependencies on the request path
A CRM or ERP lookup that takes several seconds pushes p90 latency past Meta's alert levels and gets the Flow throttled. Cache reference data, set tight upstream timeouts and precompute where you can.
Ignoring health-check pings
pingrequests arrive even when nobody is using the Flow. An endpoint that only handles real actions looks unavailable and drags its Flow towards Blocked.Guessable or reusable flow tokens
The
flow_tokenis your correlation key. Make it unguessable, map it server-side to the customer and Flow, expire it, and return 427 for stale tokens so the customer can be sent a fresh Flow. Ondata_api_version4.0, also verifyflow_token_signatureso a replayed or forged token is rejected.
Example
Example: Live test-drive slots in Pune
A Pune car dealership sends a WhatsApp Flow for booking test drives. When the customer opens it, the INIT request asks the endpoint for showrooms near the customer's saved PIN code; after they pick a model and a showroom, a data_exchange call returns only the slots still free that week from the dealer's calendar system. When two customers race for the same 11 am slot, the slower one gets the slot screen back with an error_message — "That slot was just taken, please pick another". On confirmation the endpoint answers with the SUCCESS screen and the flow_token, and the booking reaches the messages webhook as an nfm_reply. Each call is answered in about 300 ms from a cached calendar, well inside Meta's one-second target.
Why it matters
Why does a Flow endpoint matter?
The endpoint is what turns a Flow from a static form into a small app: real slots instead of a free-text "preferred time", a validated policy number instead of a typo discovered tomorrow. It is also the part that can take a Flow down. WhatsApp monitors endpoint error rate, latency and availability, and a slow or failing endpoint gets the Flow throttled and then blocked — for every customer, including those who already received it. Treat it as a production API with its own latency budget, not a webhook you can answer lazily.
Notes
Best practices for a Flow endpoint
- Keep the private key in a secrets manager, never in the repository; after rotating it, upload the new public key straight away — clients holding the old one recover through 421.
- Log decrypted payloads only with personal data masked.
- Test against a draft Flow sent with
mode: "draft"before publishing. - Load-test the endpoint at campaign volume; one popular Flow can turn into hundreds of requests a minute.
Questions
Flow Endpoint FAQs
- Do all WhatsApp Flows need an endpoint?
- No. Only Flows that load live data or validate input mid-flow need one. A static Flow such as a survey or lead form collects answers on the device and sends them to your webhook when the customer submits, with no endpoint calls in between.
- Why does my Flow endpoint return error 421?
- 421 is the status your endpoint returns when it cannot decrypt a request; WhatsApp then re-downloads your public key and retries. If it keeps happening, the uploaded public key does not match your private key or was not uploaded for this phone number.
- What is the difference between a Flow endpoint and a webhook?
- A Flow endpoint answers synchronous, encrypted requests while a customer is inside a Flow; a webhook receives asynchronous event notifications. The final Flow submission arrives at your webhook as an
nfm_replymessage, not at the endpoint. - How fast must a Flow endpoint respond?
- Requests time out after 10 seconds, but Meta advises aiming for under 1 second. Latency alerts fire when p90 latency crosses 1, 5 or 7 seconds over 30 minutes, and sustained problems get the Flow throttled and then blocked.