Skip to content
man glossary/flow-endpoint

glossary define flow-endpoint

Name

Flow Endpoint

A Flow endpoint is the HTTPS server a WhatsApp Flow calls to load or validate data mid-flow, with every request and response encrypted using RSA and AES-GCM.

Synopsis

WhatsApp Flows endpoint, Flow data endpoint, Flows data channel, endpoint_uri

Section

Bots, flows & AI

Level

expert

▍

Reviewed

Who can read a data_exchange request?

5 / 5

Stage 5 of 5 · Unlock

Only the recipient's device has the key, so only it can turn the ciphertext back into the message.

At Your Flow endpoint:{"action":"data_exchange","screen":"PICK_SLOT"}

Customer's WhatsAppsees

{"action":"data_exchange","screen":"PICK_SLOT"}
Can read itWrote it, holds it readable

Relay serversees

Ciphertext, unreadable without the key

Cannot read itRelays it, has no key

Your Flow endpointsees

{"action":"data_exchange","screen":"PICK_SLOT"}
Can read itDecrypted on the device

Can read the message

  • The customer's WhatsApp app
  • Your endpoint, which holds the RSA private key

Cannot read it

  • Anyone watching the network
  • Any relay without your private key
Step through one request: the WhatsApp client locks it with a one-time AES key wrapped in your RSA public key, relays carry only ciphertext, and only your endpoint's private key opens it.

Step 5 of 5, Unlock: Only the recipient's device has the key, so only it can turn the ciphertext back into the message.

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 ping health checks with status active, 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 manager
passphrase: 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 tag
const 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 bit
const 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 key
const { 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 notification
else 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?

  1. 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 confirm business_public_key_signature_status is VALID with a GET.

  2. step 2 — Register the endpoint

    Set endpoint_uri on the Flow through the Flows API or Flow Builder, and declare data_api_version and routing_model in the Flow JSON. The URL must be public HTTPS with a valid certificate.

  3. step 3 — Verify and decrypt every request

    Check the X-Hub-Signature-256 HMAC 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+ and data_api_version 4.0, the payload also carries flow_token_signature, a JWT (HS256) signed with your app secret: verify it and check the token inside matches flow_token.

  4. step 4 — Handle the action

    Answer ping with {"data": {"status": "active"}}. For INIT, BACK and data_exchange, return the next screen and its data, or the same screen with error_message to show the customer a problem. Acknowledge client error notifications with {"data": {"acknowledged": true}}, and return 427 for a flow_token that is no longer valid.

  5. 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.

  6. step 6 — Watch the health alerts

    Subscribe to the flows webhook field and alarm on ENDPOINT_ERROR_RATE, ENDPOINT_LATENCY and ENDPOINT_AVAILABILITY events — 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

    ping requests 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_token is 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. On data_api_version 4.0, also verify flow_token_signature so 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_reply message, 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.

See InfiQ pricing

Put Flow Endpoint to work

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.