# WhatsApp Flows

> Build, publish, and manage WhatsApp Flows - structured forms customers fill out inside WhatsApp - from the Flows page in Avox.

Source: https://docs.theavoxai.com/flows

A **Flow** is a form that opens inside a WhatsApp chat - booking an appointment, capturing a
lead, running a short survey - so your customer never leaves the conversation. Flows are Meta's
own WhatsApp feature; Avox gives you premade Flows, a JSON editor with a live phone preview, and
one-click publishing to Meta.

This two-screen preview is rendered by the same component the Flow wizard uses while you edit.

For how Flows work on Meta's side - Flow JSON, screens, actions, and the status lifecycle - see
[Flows on WhatsApp](https://docs.theavoxai.com/whatsapp/flows).

## Before you start

- **Connect a WhatsApp number.** Flows belong to the WhatsApp Business Account behind your number,
  and creating one needs a connected number ([AVX-3034](https://docs.theavoxai.com/error-codes/avx-3034)).
- **Check your plan's Flow limit.** Each Switchboard can hold as many Flows as its plan allows
  (see [Plans](https://docs.theavoxai.com/plans)). Every Flow listed in **Your Flows** uses a slot, whatever its status.
  When every slot is used, the page shows "You've used every Flow your plan allows. (AVX-3033)".

## Who can do what

| Action | Admin | Manager |
| --- | --- | --- |
| View Flows, drafts, and metrics | Yes | Yes |
| Create and publish a Flow | Yes | Yes |
| Validate JSON, register an encryption key, run a health check | Yes | Yes |
| Delete a saved wizard draft | Yes | Yes |
| Delete a **Draft on Meta** Flow or deprecate a published one | Yes | Only with the **Delete Flows** permission (off by default) |
| Refresh Flows from Meta | Yes | Yes |
| Receive Flow status emails | Yes | - |

The **Manager** column shows the Manager defaults. An Admin can give or take away Flow permissions
person by person - see [Permissions](https://docs.theavoxai.com/switchboard/members#permissions).

If **Create Flow** doesn't appear for you, Flow creation has been switched off for your account -
contact [Support](https://docs.theavoxai.com/support).

## The Flows page

Open **Flows** in the sidebar.

- **Flows used** - how many of your plan's Flow slots are in use, with a breakdown by status
  underneath (for example "3 published · 1 draft on meta"). **Docs** opens this page and **Create
  Flow** opens the wizard.
- **Start from a premade Flow** - ready-made Flows from Avox that work as plain forms, marked
  **Avox AI (Beta)**. Click one to start a draft with everything filled in, then tweak it.
- **Needs a live endpoint** - premade Flows that pull real-time data from your own server
  (available slots, a live quote, and so on). Each card carries a **Needs an endpoint** badge, and
  you're asked for your server's URL.
- **Drafts** - Flows you started but haven't published, "Drafts (n/10)". **Edit** resumes one
  where you left off; the trash icon deletes it.
- **Your Flows** - every Flow, with its **Name** (and "Endpoint-backed" when it uses an endpoint),
  **Category**, **Status**, **Last edited**, and a **Manage** button. **Refresh from Meta** asks
  Meta for every Flow's latest status straight away. With no Flows yet it reads "No Flows yet -
  create one from a premade template above, or refresh in case one already exists on Meta's
  side."

A brand-new Switchboard with no Flows and no drafts also sees **Get started with your first
Flow** at the top of the page.

### Premade Flows

| Premade Flow | Category | Needs an endpoint |
| --- | --- | --- |
| Collect purchase interest | Shopping | No |
| Get feedback | Other | No |
| Send a survey | Survey | No |
| Customer support | Customer support | No |
| Get leads for a pre-approved loan/credit card | Lead generation | Yes |
| Provide insurance quote | Lead generation | Yes |
| Capture interest for a personalised offer | Lead generation | Yes |
| Account sign-in/sign-up | Log in | Yes |
| Appointment booking | Appointment booking | Yes |

## Creating a Flow

> **Tip**
>
> Want the short version? See the [Create a Flow](https://docs.theavoxai.com/walkthrough/create-flow) walkthrough.

Click **Create Flow** to open the wizard at `/flows/new` ("Published straight to Meta."). It has
three steps - **Type**, **Name**, and **Content** - with a live phone preview on the right. The
wizard saves your work as a draft a moment after each change (up to 10 drafts per Switchboard;
see [Drafts](#drafts)).

### Step 1: Type

- **Start from scratch** - pick one of Meta's nine categories: Sign up, Log in, Appointment
  booking, Lead generation, Shopping, Contact us, Customer support, Survey, or Other. The preview
  shows a simple one-field form for that category, and the editor starts with a blank one-screen
  Flow.
- **Or start from a premade Flow** - pick one of the premade Flows above, split into **No endpoint
  needed** and **Needs a live endpoint**.

Hover or click a tile to preview it. Nothing is chosen until you click **Continue**.

### Step 2: Name

| Field | Rules |
| --- | --- |
| **Flow name** | Required, up to 100 characters. This is the name you'll see in Meta's Flow list. A premade Flow fills in its own title, which you can change. |
| **Categories** | Pick at least one. A Flow can belong to several categories. |

Step 2 is headed **Name your Flow**. Click **Next** to continue.

### Step 3: Content

The **Flow content Editor (JSON)** is a full code editor with line numbers, syntax highlighting,
and right-click commands such as Format Document. Above it:

- **Run** - an instant check in your browser that redraws the preview from your latest JSON. Press
  Ctrl+Enter (Cmd+Enter on a Mac) for the same thing. If the JSON doesn't parse, you see "Invalid
  JSON" with the reason.
- **Validate** - a real check against Meta's own validator. Meta's findings appear in the
  **Errors** panel.

Three buttons in the footer open panels above it:

- **Errors** - one numbered list of every problem: JSON that doesn't parse, structure problems
  Avox spots as you type (no screens, a duplicate screen id, a screen with no Form, a Form with no
  Footer, a Footer with no action), and whatever Meta's validator found. With nothing to report it
  says "No problems found. Run Validate above to also check against Meta itself."
- **Actions** - every action your JSON uses, how many times, and what it does in plain English.
  See [Flows on WhatsApp](https://docs.theavoxai.com/whatsapp/flows#actions).
- **Endpoint** - the switch **This Flow needs a live endpoint**, and a checklist for setting one
  up. See [Live endpoints](#live-endpoints).

When the Flow has more than one screen, a **Previewing** menu above the phone picks which screen
the preview shows.

## Publishing

Click **Create & publish**. Avox checks that the Flow has a name ("Give this Flow a name"), at
least one category ("Pick at least one category"), valid JSON, and (when the endpoint switch is
on) an endpoint URL ("Enter a live endpoint URL"). Then Avox creates the Flow on Meta, uploads its
JSON, and publishes it in one go. You see "Flow created and published" and return to the Flows
page, where the Flow shows as **Published**. Its draft is removed.

If Meta's validator rejects the JSON, nothing is published: you see "Meta rejected the Flow's
content" ([AVX-3035](https://docs.theavoxai.com/error-codes/avx-3035)) and the attempt is listed as **Failed**. Run
**Validate** to see Meta's exact findings in the **Errors** panel, fix them, and publish again.

> **Tip**
>
> A **Failed** attempt still exists on Meta as a draft and uses one of your Flow slots. Click
> **Refresh from Meta** on the Flows page: it then shows as **Draft on Meta**, and you can open it
> and click **Delete Flow** to free the slot.

**Create & publish** is disabled while every Flow slot on your plan is in use.

## Live endpoints

Some Flows fetch live data from your own server while the customer fills them in - available
appointment slots, a quote, a sign-in check. Meta calls your server directly, encrypted, at the
**endpoint URL** you set. Avox sets up the connection with Meta; your server handles the requests.

Open the **Endpoint** panel and switch on **This Flow needs a live endpoint**. The checklist reads
"Endpoint is not set up" and shows how many of its five steps are done ("Complete the steps below
(3/5)"). Once all five are done it reads "Endpoint is set up":

**1. Set endpoint URI**

Enter your server's URL, for example `https://your-server.com/flow-endpoint`. It must start
with `https://` and point to a public address ([AVX-3041](https://docs.theavoxai.com/error-codes/avx-3041)).

**2. Add phone number**

Shows the connected number this Flow uses, or a **Connect one** link to
[Connections](https://docs.theavoxai.com/connections).

**3. Sign public key**

Click **Generate key pair**. Your browser creates an RSA key pair; the private key never leaves
your browser. Copy it with **Copy private key** and store it as a secret on your server - "Save
this private key now - it won't be shown again". Then click **Register public key with Meta**.
Once Meta accepts it, the step shows **Registered with Meta**.

**4. Connect Meta app**

Ticks itself when your WhatsApp number is connected.

**5. Health check**

Click **Run health check** (it's enabled once the endpoint URL is valid). Avox sends a test
request to your URL and shows how it answered, for example "Responded 200 in 140ms", or the
reason it couldn't. "A plain reachability check, not Meta's own encrypted ping." Meta sends its
own encrypted health check later.

Under the checklist, **Endpoint snippet** has a starting point for your server in two variants.
It decrypts Meta's request, answers Meta's `ping` health check, and encrypts the response. Your
own screen-by-screen logic goes where it's marked. **Copy** copies the snippet.

```js
// app/api/flow-endpoint/route.ts - Next.js Route Handler
const crypto = require("crypto");

// PRIVATE_KEY_PEM - never send this anywhere. Store it as a secret
// (e.g. an env var), never in source control.
const PRIVATE_KEY_PEM = process.env.FLOW_PRIVATE_KEY;

function decryptRequest(body) {
  const { encrypted_flow_data, encrypted_aes_key, initial_vector } = body;
  const aesKey = crypto.privateDecrypt(
    { key: PRIVATE_KEY_PEM, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
    Buffer.from(encrypted_aes_key, "base64")
  );

  const flowDataBuffer = Buffer.from(encrypted_flow_data, "base64");
  const TAG_LENGTH = 16;
  const encryptedBody = flowDataBuffer.subarray(0, -TAG_LENGTH);
  const authTag = flowDataBuffer.subarray(-TAG_LENGTH);
  const iv = Buffer.from(initial_vector, "base64");

  const decipher = crypto.createDecipheriv("aes-128-gcm", aesKey, iv);
  decipher.setAuthTag(authTag);
  const decrypted = Buffer.concat([decipher.update(encryptedBody), decipher.final()]);
  return { decryptedBody: JSON.parse(decrypted.toString("utf-8")), aesKey, iv };
}

function encryptResponse(responseBody, aesKey, iv) {
  // Meta requires the response encrypted with the same AES key but the
  // IV bit-flipped, not reused as-is.
  const flippedIv = Buffer.from(iv.map((byte) => ~byte));
  const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, flippedIv);
  const encrypted = Buffer.concat([cipher.update(JSON.stringify(responseBody), "utf-8"), cipher.final()]);
  return Buffer.concat([encrypted, cipher.getAuthTag()]).toString("base64");
}

function handleFlowRequest(body) {
  const { decryptedBody, aesKey, iv } = decryptRequest(body);

  // Meta sends this before trusting the endpoint at all - respond with
  // exactly this shape or the "Health check" step above will keep failing.
  if (decryptedBody.action === "ping") {
    return encryptResponse({ data: { status: "active" } }, aesKey, iv);
  }

  // Your real per-screen business logic goes here - read
  // decryptedBody.screen / decryptedBody.data, decide the next screen.
  const responseBody = {
    version: decryptedBody.version,
    screen: "SUCCESS",
    data: { extension_message_response: { params: { flow_token: decryptedBody.flow_token } } },
  };
  return encryptResponse(responseBody, aesKey, iv);
}

export async function POST(request) {
  try {
    const body = await request.json();
    const encryptedResponse = handleFlowRequest(body);
    return new Response(encryptedResponse, { headers: { "Content-Type": "text/plain" } });
  } catch (error) {
    console.error("[flow-endpoint] failed to process request", error);
    return new Response(null, { status: 421 }); // Meta re-fetches the public key on 421
  }
}
```

```js
// endpoint.js - Express route for this Flow's data_endpoint_uri
const crypto = require("crypto");

// PRIVATE_KEY_PEM - never send this anywhere. Store it as a secret
// (e.g. an env var), never in source control.
const PRIVATE_KEY_PEM = process.env.FLOW_PRIVATE_KEY;

function decryptRequest(body) {
  const { encrypted_flow_data, encrypted_aes_key, initial_vector } = body;
  const aesKey = crypto.privateDecrypt(
    { key: PRIVATE_KEY_PEM, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
    Buffer.from(encrypted_aes_key, "base64")
  );

  const flowDataBuffer = Buffer.from(encrypted_flow_data, "base64");
  const TAG_LENGTH = 16;
  const encryptedBody = flowDataBuffer.subarray(0, -TAG_LENGTH);
  const authTag = flowDataBuffer.subarray(-TAG_LENGTH);
  const iv = Buffer.from(initial_vector, "base64");

  const decipher = crypto.createDecipheriv("aes-128-gcm", aesKey, iv);
  decipher.setAuthTag(authTag);
  const decrypted = Buffer.concat([decipher.update(encryptedBody), decipher.final()]);
  return { decryptedBody: JSON.parse(decrypted.toString("utf-8")), aesKey, iv };
}

function encryptResponse(responseBody, aesKey, iv) {
  // Meta requires the response encrypted with the same AES key but the
  // IV bit-flipped, not reused as-is.
  const flippedIv = Buffer.from(iv.map((byte) => ~byte));
  const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, flippedIv);
  const encrypted = Buffer.concat([cipher.update(JSON.stringify(responseBody), "utf-8"), cipher.final()]);
  return Buffer.concat([encrypted, cipher.getAuthTag()]).toString("base64");
}

function handleFlowRequest(body) {
  const { decryptedBody, aesKey, iv } = decryptRequest(body);

  // Meta sends this before trusting the endpoint at all - respond with
  // exactly this shape or the "Health check" step above will keep failing.
  if (decryptedBody.action === "ping") {
    return encryptResponse({ data: { status: "active" } }, aesKey, iv);
  }

  // Your real per-screen business logic goes here - read
  // decryptedBody.screen / decryptedBody.data, decide the next screen.
  const responseBody = {
    version: decryptedBody.version,
    screen: "SUCCESS",
    data: { extension_message_response: { params: { flow_token: decryptedBody.flow_token } } },
  };
  return encryptResponse(responseBody, aesKey, iv);
}

app.post("/flow-endpoint", express.json(), (req, res) => {
  try {
    const encryptedResponse = handleFlowRequest(req.body);
    res.set("Content-Type", "text/plain").send(encryptedResponse);
  } catch (error) {
    console.error("[flow-endpoint] failed to process request", error);
    res.status(421).send(); // Meta re-fetches the public key on 421
  }
});
```

## Drafts

The wizard saves your work a moment after each change, and the page address becomes
`/flows/new?draft=...` so a reload picks up where you were.

- Up to **10 drafts** per Switchboard. At 10, saving stops with [AVX-3043](https://docs.theavoxai.com/error-codes/avx-3043);
  delete a draft to free a slot.
- Picking a premade Flow on the Flows page always starts a new draft
  ([AVX-3045](https://docs.theavoxai.com/error-codes/avx-3045) if that fails).
- **Edit** on a draft card reopens it at its step ([AVX-3044](https://docs.theavoxai.com/error-codes/avx-3044) if it can't be
  loaded).
- **Start over** in the wizard discards the draft and opens a blank wizard.
- A draft is removed automatically once its Flow is published.

## Managing a Flow

Click **Manage** on a Flow to open its page. **Back to Flows** returns to the list. It shows the
Flow's name, its **Meta Flow ID**, its **Category**, its **Status**, and (for an endpoint-backed
Flow) its **Endpoint** URL. A **Draft on Meta** Flow has a **Delete Flow** button and a
**Published** Flow has **Deprecate Flow**. Any other status shows "Only a Flow still in draft on
Meta's side can be deleted here. (AVX-3039)" in their place.

### Statuses

| Status | Meaning |
| --- | --- |
| **Published** | Live. Customers can open it, and automations can send it. |
| **Draft on Meta** | Meta's copy is still a draft. This is the only status a Flow can be deleted from. |
| **Failed** | Meta didn't accept the JSON or the publish step when you created it. |
| **Deprecated** | Retired - by you or by Meta. It can't be sent again. |
| **Throttled** | Meta has temporarily limited how often it can be sent. |
| **Blocked** | Meta has stopped it from being sent. |

Meta tells Avox when a Flow's status changes, and **Refresh from Meta** fetches every status
straight away ([AVX-3042](https://docs.theavoxai.com/error-codes/avx-3042) if Meta can't be reached). When a Flow becomes
**Blocked**, **Throttled**, or **Deprecated**, Avox emails every Admin of the Switchboard (part of
the **WhatsApp account alerts** preference on [Compliance](https://docs.theavoxai.com/compliance)).

### Deleting a draft Flow

On a Flow whose status is **Draft on Meta**, click **Delete Flow**. Avox removes it from Meta and
from Avox, shows "Flow deleted", and returns you to the Flows page. A published Flow can't be
deleted - Meta only allows retiring it ([AVX-3039](https://docs.theavoxai.com/error-codes/avx-3039)).

### Deprecating a published Flow

**1. Click Deprecate Flow**

Shown on **Published** Flows only.

**2. Read the dialog**

**Deprecate "flow_name"?** - "Meta doesn't allow deleting a published Flow - deprecating
retires it instead. This can't be undone, and the Flow will stop being usable in any message
that references it."

**3. Confirm**

Click **Deprecate Flow**. You see "Flow deprecated" and the status changes to **Deprecated**.

> **Warning**
>
> Automations that send a deprecated or deleted Flow can't be switched on until you pick a
> published Flow for that action ([AVX-3078](https://docs.theavoxai.com/error-codes/avx-3078)).

### Metrics

Endpoint-backed Flows show **Endpoint requests - last 30 days**: the **Total requests** Meta sent
to your endpoint and a daily bar chart, straight from Meta. With no traffic yet it reads "No
endpoint traffic in the last 30 days yet." A Flow without an endpoint shows a short note in its
place, since Meta reports endpoint traffic only.

### Responses

When a customer completes a Flow, their answers arrive in Avox. The **Flows** card on the
dashboard's [Overview](https://docs.theavoxai.com/overview) page shows **Responses (30d)**, the number of completed
responses in the last 30 days, next to your Flows broken down by status.

## Sending a Flow

A published Flow is sent from an automation's **Use Flows** action, with a button label of your
choice (for example "Book now"). See [Actions](https://docs.theavoxai.com/automation/actions).

> **Tip**
>
> For a short exchange - one or two questions, a confirm or reschedule choice - an automation's
> **Ask a question** and **Wait for reply** steps are often simpler than a Flow: the customer
> answers right in the chat, and the answer is saved to a tag or contact field. Use a Flow when
> you need a full form with several fields on one screen. See [Actions](https://docs.theavoxai.com/automation/actions).

## Related errors

| Code | Message |
| --- | --- |
| [AVX-3032](https://docs.theavoxai.com/error-codes/avx-3032) | Check the Flow's name, categories, and JSON. |
| [AVX-3033](https://docs.theavoxai.com/error-codes/avx-3033) | You've used every Flow your plan allows. |
| [AVX-3034](https://docs.theavoxai.com/error-codes/avx-3034) | Connect a WhatsApp number before creating Flows. |
| [AVX-3035](https://docs.theavoxai.com/error-codes/avx-3035) | Meta couldn't accept that Flow. Check its JSON and try again. |
| [AVX-3036](https://docs.theavoxai.com/error-codes/avx-3036) | Meta rejected this Flow's JSON - check the errors below. |
| [AVX-3037](https://docs.theavoxai.com/error-codes/avx-3037) | Couldn't publish that Flow. Try again. |
| [AVX-3038](https://docs.theavoxai.com/error-codes/avx-3038) | Couldn't delete that Flow. Try again. |
| [AVX-3039](https://docs.theavoxai.com/error-codes/avx-3039) | Only a Flow still in draft on Meta's side can be deleted here. |
| [AVX-3040](https://docs.theavoxai.com/error-codes/avx-3040) | This Flow template needs a live endpoint URL to work. |
| [AVX-3041](https://docs.theavoxai.com/error-codes/avx-3041) | Enter a valid HTTPS endpoint URL. |
| [AVX-3042](https://docs.theavoxai.com/error-codes/avx-3042) | Couldn't refresh Flows from Meta. Try again. |
| [AVX-3043](https://docs.theavoxai.com/error-codes/avx-3043) | You've reached the maximum of 10 saved drafts. Delete one to start another. |
| [AVX-3044](https://docs.theavoxai.com/error-codes/avx-3044) | Couldn't load that draft. Try again. |
| [AVX-3045](https://docs.theavoxai.com/error-codes/avx-3045) | Couldn't start a new draft from that template. Try again. |
| [AVX-3046](https://docs.theavoxai.com/error-codes/avx-3046) | Couldn't generate a key pair in this browser. Try again. |
| [AVX-3047](https://docs.theavoxai.com/error-codes/avx-3047) | Meta couldn't register that public key. Try again. |
| [AVX-3048](https://docs.theavoxai.com/error-codes/avx-3048) | Couldn't reach that endpoint URL. |
| [AVX-3049](https://docs.theavoxai.com/error-codes/avx-3049) | Couldn't deprecate that Flow. Try again. |
| [AVX-3050](https://docs.theavoxai.com/error-codes/avx-3050) | Couldn't load metrics for this Flow. |
