> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pilotstatus.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Flows API Reference

> REST endpoints to create, edit, publish, clone and deprecate WhatsApp Flows, read form responses, and configure the data_exchange endpoint key.

Every endpoint below authenticates with a **number-scoped API key** (`x-api-key`) whose number is a **Meta (Cloud API)** number. The WABA is derived from that number — there is no parameter to name one, by design.

| Permission     | Covers                                                               |
| -------------- | -------------------------------------------------------------------- |
| `flows:read`   | Listing Flows and reading form responses                             |
| `flows:manage` | Every write: create, update, upload JSON, publish, deprecate, delete |

<Note>
  `flows:read` is a slug of its own and is **not** `templates:read`. A template listing returns text your workspace wrote; a Flow response returns what a **customer** typed into a form — a name, a phone, sometimes a document number. The two are gated separately on purpose.
</Note>

## List Flows

```bash theme={null}
curl "https://pilotstatus.com.br/v1/flows?page=1&pageSize=30" \
  -H "x-api-key: ps_your_key_here"
```

```json theme={null}
{
  "flows": [
    {
      "id": "flw_local_1",
      "metaFlowId": "1122334455",
      "name": "Agendamento",
      "metaStatus": "PUBLISHED",
      "categories": ["APPOINTMENT_BOOKING"],
      "jsonVersion": "7.1",
      "healthStatus": "AVAILABLE",
      "clonedFromFlowId": null,
      "supersededByFlowId": null,
      "createdAt": "2026-08-30T12:00:00.000Z",
      "updatedAt": "2026-09-01T09:12:00.000Z"
    }
  ],
  "total": 1, "page": 1, "pageSize": 30
}
```

<Note>
  `id` is the **local id** and is the only id you ever send back to us. `metaFlowId` is Meta's own and is read-only — it is `null` while a Flow exists here but has not reached Meta.
</Note>

## Create or clone

```bash theme={null}
curl -X POST "https://pilotstatus.com.br/v1/flows" \
  -H "x-api-key: ps_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Agendamento v2",
    "categories": ["APPOINTMENT_BOOKING"],
    "cloneFromFlowId": "flw_local_1"
  }'
```

Returns `201` with the new Flow in `DRAFT`. Omit `cloneFromFlowId` to start from scratch.

<Warning>
  `cloneFromFlowId` is the **local id** of the source Flow — the `id` field, not `metaFlowId`. Cloning is the only way to change a Flow that has been published.
</Warning>

## Rename and re-categorize

```bash theme={null}
curl -X PATCH "https://pilotstatus.com.br/v1/flows/flw_local_1" \
  -H "x-api-key: ps_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Agendamento (matriz)", "categories": ["APPOINTMENT_BOOKING"] }'
```

Accepts **only** `name` and `categories`. This never publishes and never deprecates — those have endpoints of their own, so that a mistake in a form cannot reach an irreversible operation.

## Upload the Flow JSON

```bash theme={null}
curl -X POST "https://pilotstatus.com.br/v1/flows/flw_local_1/json" \
  -H "x-api-key: ps_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "flowJson": "{\"version\":\"7.1\",\"screens\":[...]}" }'
```

A `200` means the document compiled, and `validationErrors` is then always empty:

```json theme={null}
{ "validationErrors": [] }
```

A document Meta refuses comes back as **`422` `FLOW_JSON_INVALID`**, with Meta's own list attached:

```json theme={null}
{
  "error": "A Meta recusou este Flow JSON. Veja validationErrors.",
  "errorEN": "Meta rejected this Flow JSON. See validationErrors.",
  "code": "FLOW_JSON_INVALID",
  "validationErrors": [ { "error": "...", "line_start": 12, "message": "..." } ]
}
```

<Warning>
  **This is deliberately NOT how Meta's own API behaves, and the difference is the point.**

  Meta answers `200` with the problems listed *inside* the payload — so an apparent success over an invalid document is the normal case there, and a client that only checks the status ships a Flow that never compiled.

  This route reads the list rather than the Graph status, and answers `422` whenever it is non-empty. A `200` from us therefore means what a `200` should mean. Read `validationErrors` on the `422` — it is Meta's own answer, verbatim.

  `flowJson` is a **string**, never a nested object: Meta compiles the exact bytes, and re-serializing would silently reorder what you wrote.
</Warning>

## Publish

```bash theme={null}
curl -X POST "https://pilotstatus.com.br/v1/flows/flw_local_1/publish" \
  -H "x-api-key: ps_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "confirm": true }'
```

<Warning>
  **Irreversible on Meta.** A published Flow can never be edited or unpublished — only cloned into a new one. `"confirm": true` is required; without it the request is refused with `400`.
</Warning>

## Deprecate

```bash theme={null}
curl -X POST "https://pilotstatus.com.br/v1/flows/flw_local_1/deprecate" \
  -H "x-api-key: ps_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "supersededByFlowId": "flw_local_2" }'
```

Retires the Flow without deleting it, and without deleting the responses people already submitted. `supersededByFlowId` is optional — but if you send it, it must name a real Flow of yours: an empty string is refused rather than silently ignored, because a broken version chain is invisible afterwards.

## Delete

```bash theme={null}
curl -X DELETE "https://pilotstatus.com.br/v1/flows/flw_local_1" \
  -H "x-api-key: ps_your_key_here"
```

Only a `DRAFT` can be deleted. Anything else returns `409`.

## Read form responses

```bash theme={null}
curl "https://pilotstatus.com.br/v1/flows/flw_local_1/responses?page=1&pageSize=30" \
  -H "x-api-key: ps_your_key_here"
```

Returns what customers submitted, joined to the contact who answered, within the retention window. Responses past their expiry are never served.

## `data_exchange` endpoint key

<Note>
  **Not yet exposed on the public API.** The key management for the `data_exchange` endpoint — generating the keypair, registering the public half with Meta, and reading back whether Meta still holds ours — is built, but has no REST endpoint yet. This section will list it here the day it does.

  What the feature does, and the design behind it, is described in [Flows](/concepts/flows). If you need it before it is exposed, talk to support.
</Note>

## Errors

Every failure of this surface answers the same envelope — there is exactly one shape to read, and one spelling:

```json theme={null}
{ "error": "<Portuguese>", "errorEN": "<English>", "code": "<CODE>" }
```

`FLOW_JSON_INVALID` adds a `validationErrors` array; `FLOW_UNKNOWN_FIELDS` adds the offending keys. Never assume the set of codes is closed — branch on the ones you handle and fall through to `code` for the rest.

| Code                                 | Status | Meaning                                                 |
| ------------------------------------ | ------ | ------------------------------------------------------- |
| `FLOW_NOT_FOUND`                     | 404    | No Flow with that id inside your key's WABA             |
| `FLOW_NOT_DRAFT`                     | 409    | The Flow left `DRAFT` — clone it to start a new version |
| `FLOW_REQUIRES_META_NUMBER`          | 422    | The key's number is not a Meta number, or has no WABA   |
| `FLOW_NUMBER_FROM_KEY`               | 422    | The number could not be derived from the key            |
| `FLOW_JSON_INVALID`                  | 422    | Meta refused the document — read `validationErrors`     |
| `FLOW_JSON_REQUIRED`                 | 400    | `flowJson` absent or empty                              |
| `FLOW_NAME_REQUIRED`                 | 400    | `name` missing on create                                |
| `FLOW_NAME_INVALID`                  | 400    | `name` present but not a usable string                  |
| `FLOW_CATEGORY_INVALID`              | 400    | A category outside the set Meta accepts                 |
| `FLOW_CLONE_SOURCE_INVALID`          | 400    | `cloneFromFlowId` is not a usable local id              |
| `FLOW_PATCH_EMPTY`                   | 400    | `PATCH` with neither `name` nor `categories`            |
| `FLOW_UNKNOWN_FIELDS`                | 400    | The body carried a field the route does not accept      |
| `FLOW_PUBLISH_REQUIRES_CONFIRMATION` | 400    | `publish` without `"confirm": true`                     |
| `FLOW_DEPRECATE_INVALID_SUCCESSOR`   | 400    | `supersededByFlowId` present but blank or unknown       |
| `FLOW_BODY_INVALID`                  | 400    | Malformed body, or a required field missing             |
| `INVALID_PAGINATION`                 | 400    | `page` × `pageSize` past the deep-offset ceiling        |
| `INTERNAL_ERROR`                     | 500    | Ours. Retry, then tell support the code                 |

<Note>
  The last two carry **no `FLOW_` prefix** — they are shared across the public API and keep their global spelling. A client matching on `code.startsWith("FLOW_")` would miss both.
</Note>

## Related

* [Flows](/concepts/flows)
* [Templates](/concepts/templates)
* [API Overview](/api/overview)
