# ChatFood Flow API: full guide for AI agents Base URL: `https://api.chatfood.app`. Every public route lives under the version prefix `/v1` (for example `GET /v1/pages`). There is no `/api` prefix. Paths are lower case, with hyphens in compound names (`/v1/variable-groups`). This guide is written from the source of Flow.API (the control plane) and chatfood.bot (the runtime that executes flows and talks to Meta and Telegram). When this text and the OpenAPI document disagree, the OpenAPI document is generated from code and wins for field names; this guide wins for semantics, limits and the flow script contract, which the OpenAPI document does not describe (the script is an opaque string to it). Companion resources: - `/llms.txt`: short index. - `/.well-known/agent.json`: machine-readable manifest (auth, capabilities, endpoints). - `/swagger/agents/swagger.json`: OpenAPI 3 with ONLY the `/v1` routes an API token can call, in every environment. Each operation carries `x-required-scope` (`area:access`, or `any` when every active token may call it). Prefer this document: it is generated with the same rule the server enforces, so a route missing from it answers `403` to a token. The reference at `/docs` and the Postman collection at `/docs/postman.json` are built from it. - `/swagger/v1/swagger.json`: OpenAPI 3 for user sessions. It also lists the routes that existed before `/v1`, marked `deprecated` and pointing at their `/v1` successor. Do not build on them: they will be removed once nobody calls them. - `/graphql`: read-only analytics. Introspection enabled. --- ## 1. Conventions - JSON in, JSON out, property names in camelCase. Enums are integers in bodies and responses (a query-string filter also accepts the enum name). - Dates are ISO 8601. Unless stated otherwise they are UTC. Broadcast `scheduledTo` is the exception (page-local wall clock, see §7). - Ids are GUIDs. A page is ALWAYS addressed by its GUID (`pageId`), never by the provider id. The provider id is exposed as `referenceId` for information only. A lead's provider id (PSID on Meta, chat id on Telegram) is `externalId`. - The account comes from the credential. When the credential reaches more than one account, pass `?accountId=`: reads without it span every account you reach, and writes that create something answer `400` asking for it. The account is part of the path only for the account's own resources (`/v1/accounts/{accountId}/...`). - CRUD follows the HTTP verb: `GET` reads, `POST` creates (`201` with the resource), `PUT` replaces the whole resource, `PATCH` changes only the fields you send: a missing field is left as it is, an explicit `null` clears an optional field and is refused with `400` on a required one (two exceptions keep their own rule: `PATCH /v1/pages/{pageId}`, where `""` clears a text and an empty GUID clears the category or group, and `PATCH /v1/meta/apps/{appId}`, where `null` keeps the value), `DELETE` removes (`204`); a batch `DELETE ...?ids=` of variables, slugs or UTM answers `200 {"deleted": n}` with the rows actually removed. An action that is not CRUD is a sub-resource with a verb, as the exception: `POST /v1/broadcasts/{broadcastId}/cancel`, `POST /v1/schemas/{schemaId}/copy`. - Lists take `?limit=` (1 to 200, default 50; a few lists cap it lower and say so), `?cursor=` and `?sort=` (a field of the list, `-` in front for descending, for example `?sort=-createdAt`), and answer `{"items": [...], "nextCursor": "..."}`. Pass `nextCursor` back as `cursor` for the next page; it is `null` on the last page. The cursor is opaque: do not build or edit it. A list that already counts its whole filter to page (pages, categories, connections, Meta apps and their pages, journeys, alerts, broadcasts) adds `total` next to `items`; `GET /v1/schemas`, which pages by a keyset cursor without counting, adds it only on `?includeTotal=true` (one more count query). - Batches: `PUT` on the collection upserts many items at once; `DELETE` on the collection with `?ids=a&ids=b` (in the query, never in the body) removes many. - Errors are `application/problem+json` (RFC 9457): `{"type", "title", "status", "detail"}`, sometimes with extra fields (`invalid`, `errors`). `401` means the credential is missing, expired or revoked; `403` means the credential is valid but the resource belongs to an account it cannot use or, for an API token, the route needs a scope the token lacks or is not available to tokens at all (the `detail` names the missing scope). GraphQL resolvers return an EMPTY result, not an error, when the caller does not own the requested pages. - Partial success is normal: `POST /v1/broadcasts`, `POST /v1/journeys` and `POST|PUT /v1/schemas` drop pages the caller does not own (or that are deleted) and report them in `skippedPageIds` (schemas: they are removed from `pages`). If none of the pages is owned, the call fails with `403`. - API tokens are rate limited per token (default 120 requests per minute per API instance); over the limit the answer is `429` with `Retry-After`. User JWT sessions are not limited. The bot already paces delivery to Meta. - CORS is an allow-list of ChatFood web origins. Call the API from a server or CLI, not from a browser on another origin. - Versioning: `/v1` changes only in ways that do not break existing clients (new routes, new optional fields, new response fields, new enum values). A breaking change ships as `/v2`, announced in the changelog. --- ## 2. Authentication ### 2.1 API token (recommended for agents) An API token is a long-lived credential bound to a user, to a set of scopes and, optionally, to ONE of that user's accounts. Format: `cf_live_` followed by 43 URL-safe base64 characters (regex `cf_live_[A-Za-z0-9_-]{43}`, so secret scanners can find a leaked one). Send it in either header: ``` Authorization: Bearer cf_live_... X-Api-Key: cf_live_... ``` Scopes follow `area:access`, where access is `read` or `write` and `write` implies `read`: | Scope area | What it covers | |---|---| | `pages` | pages (`/v1/pages`), connection history (`/v1/connections`), Meta and Telegram page connection, ice breakers, page variables, alerts | | `leads` | leads (`/v1/leads`), lead tags (`/v1/tags`), deleting the leads of a page | | `flows` | flows (`/v1/schemas`), utility templates, Meta message templates, media upload (`/v1/uploads`, `/v1/voice-messages`), `POST /v1/schemas/{schemaId}/test` | | `broadcasts` | `/v1/broadcasts` and its executions | | `journeys` | `/v1/journeys` | | `metrics` | `/v1/alerts/summary`, `/v1/schemas/{schemaId}/metrics*`, `/v1/pages/{pageId}/send-time-suggestion` and the GraphQL endpoint; `broadcastCalendar*` also needs `broadcasts:read` and `profileCalendar` also needs `leads:read` (without them the result is empty) | | `websites` | `/v1/website-domains` and `/v1/websites` | | `pixels` | `/v1/meta/datasets` | | `payments` | reserved for Pix reports (not published yet) | | `account` | account settings, UTM and language slugs, Meta apps, notifications, categories and variable groups | | `users` | listing account members (`GET /v1/accounts/{accountId}/users`). Adding and removing members is not available to API tokens | | `billing` | `/v1/billing/*` | Rules: - `GET` needs `read`; other verbs need `write`. Exceptions are declared by the route: `POST /v1/schemas/validate-variables` needs `flows:read`; `POST /v1/website-domains/validate-script` needs `websites:read`; `POST /v1/broadcasts/delivery-preview` needs `broadcasts:read` and `POST /v1/journeys/delivery-preview` needs `journeys:read`. - A route that declares no area is not available to API tokens. So are admin routes, internal routes, token management and account or member lifecycle. The response is `403` with a `problem+json` detail naming what is missing. - A token never exceeds its owner. In each account, a request can do only what both the token scopes and the owner's permissions in that account allow. If the owner loses an account or a permission, the token loses it on the next request. - It acts as the owning user but NEVER carries administrative roles. A token created by a SUPER_ADMIN cannot reach `/admin/*` routes and cannot cross account boundaries. - If `accountId` was set at creation, the principal sees only that account: `GET /v1/accounts` returns just it, `?accountId=` must match it, and resources of other accounts answer `403`/`404`. - Only the SHA-256 hash is stored. The plaintext appears once, in the creation response. - It can expire (`expiresAt`) and can be revoked at any time. Revoked or expired tokens get `401` immediately. `lastUsedAt` and `lastUsedIp` are updated at most once per minute. - Requests are rate limited per token (default 120 per minute per API instance). Over the limit, the answer is `429` with `Retry-After`. - Every write made with a token is recorded in the audit log with the token id. Endpoints. All require a user session (JWT) except `GET /v1/me`. An impersonation session cannot create tokens (`403`): | Method | Path | Body | Response | |---|---|---|---| | POST | `/v1/api-tokens` | `{"name": "string (1-100)", "scopes": ["broadcasts:write", "metrics:read"], "accountId": "guid? (one of the user's active accounts, otherwise 400)", "expiresAt": "ISO date? (future)"}` | `201 {"id", "name", "prefix", "accountId", "scopes", "expiresAt", "createdAt", "isActive", "token": "cf_live_..."}`. `token` is returned ONLY here. `400` for an unknown scope (`invalid` lists them) or for scopes above the user's own permissions | | GET | `/v1/api-tokens` | none | `{"items": [{"id", "name", "prefix", "accountId", "scopes", "expiresAt", "lastUsedAt", "lastUsedIp", "revokedAt", "createdAt", "isActive"}], "nextCursor"}` (never the hash or the secret) | | GET | `/v1/me` | none | `{"authenticationMethod": "api_token"|"jwt", "user": {"id", "name", "email"}, "accounts": [{"id", "name", "status", "permissions": ["area:access"]}], "token": {...}|null}`. Use it to discover what the credential you hold can do in each account. `status` is 1 (active) or 0 (blocked): a blocked account takes no writes and stays out of the reads without `accountId` | | DELETE | `/v1/api-tokens/{tokenId}` | none | `204`; `404` when the token is not the caller's | Recommended bootstrap for an agent: a human logs in once (`POST /Auth`) and creates a token with the least scopes the task needs, for the right account and with an expiry. The human hands the `cf_live_` value to the agent. The agent then calls `GET /v1/me` to confirm the account and permissions, and never needs the password. ### 2.2 User session (JWT) ``` POST /Auth {"username": "", "password": ""} → 200 {"token": ""} (401 on bad credentials or inactive user) ``` Send as `Authorization: Bearer `. Lifetime 18 hours, HS256, no refresh token, no revocation: re-login when you get `401`. The JWT identifies a USER, not an account; the user may belong to several accounts (see §3). Password recovery: `POST /Auth/Recovery {"username"}`, `GET|POST /Auth/Recovery/{code}`. Login and recovery are outside `/v1`: they are the web app's session routes. --- ## 3. Domain model - **Account**: the tenant. A user belongs to one or more accounts; `GET /v1/accounts` lists them (`{"items": [{"id", "name", "pictureUrl", "pageCount", "userCount", "createdAt"}]}`). Members: `GET /v1/accounts/{accountId}/users` (`users:read`). Account-level website settings: `/v1/accounts/{accountId}/utm` (UTM value per channel) and `/v1/accounts/{accountId}/language-slugs` (the slug a language uses in the site URLs); `GET /v1/accounts/utm` and `GET /v1/accounts/language-slugs` read them for every active account at once, with `accountId` in each item. - **Page**: a connected channel: a Facebook page, an Instagram professional account or a Telegram bot. Fields: `id` (guid), `provider` (1 Facebook, 2 Instagram, 3 Telegram), `referenceId` (id at the provider, informative), `name`, `status`, `timeZone` (IANA), `language`, `currency`, `countryCode`, `categoryId`, `leadCount`, `metaAppId`, `connectedApps`, `issues`, `schemas`. Telegram pages may also own **channels** (groups or channels where the bot is admin), targetable by broadcasts. - **Category**: a niche of the account (`/v1/categories`). Pages belong to one; variable groups, website weights and dataset coverage hang on it. - **Schema** (a "flow"): the conversation definition: metadata (name, provider, status, channel trigger, keyword triggers, linked pages, translation, `conversationSettings`) plus `script`, a JSON STRING executed by the bot (§6). A flow belongs to one account and one provider, and is linked to pages of that provider. - **Lead**: a person who talked to a page (`/v1/leads`). Has `externalId`, name, email and phone when known, `tags`, and `properties` (key and value; captured answers live under `PROVIDED_*`, subscriptions under `SUBSCRIPTION_*`). - **Tag**: free text label at account level; applied to leads by flow nodes (`addTag`/`removeTag`), used for segmentation and by the `hasTagConditional` node. - **Broadcast**: one send of one PUBLISHED flow to the audience of one or more pages, immediate or scheduled, with segmentation (recency, tags, subscribers). Produces one **execution** per page. - **Journey** (a "sequence"): a multi-step drip: each step has a day offset, a time of day and one or more flows; the engine creates broadcasts per step. - **Variables**: `{{PLACEHOLDER}}` values resolved when the script is served: built-ins (`NAME`, `FIRST_NAME`, ...), page variables (`/v1/pages/{pageId}/variables`), category variables (`/v1/variable-groups/{groupId}/variables`), captured answers. `GET /v1/dynamic-variables` lists the dynamic values a variable can take. - **Channel trigger**: WHEN a flow runs: `-1 All`, `0 Inbox` (default reply to any inbound message), `1 StoryMention`, `2 Broadcast`, `3 Sequence`, `4 FirstReply` (first message ever from a lead), `5 Comment`. Telegram supports All, Inbox, Broadcast, Sequence and FirstReply. Enum reference (integers): | Enum | Values | |---|---| | ProviderType | 1 Facebook, 2 Instagram, 3 Telegram | | SchemaStatusType | -2 Deleted, -1 Desactivated, 0 Draft, 1 Published | | ChannelTrigger | -1 All, 0 Inbox, 1 StoryMention, 2 Broadcast, 3 Sequence, 4 FirstReply, 5 Comment | | TriggerMatchType | 0 Equals, 1 Contains | | BroadcastType | 1 Standard (24h window), 2 Topic (one-time notification), 3 MessageTag, 4 UtilityTemplate, 5 Automatic (per-page cascade), 6 All (Telegram only) | | BroadcastMessageTag | 1 AccountUpdate, 2 ConfirmedEventUpdate, 3 PostPurchaseUpdate, 4 CustomerFeedback, 5 HumanAgent, 6 Utility | | BroadcastRecencyFilter | 0 All, 1 Within24Hours, 2 Above24Hours, 3 Within7Days, 4 Between24HoursAnd7Days, 5 Custom | | RecencyBasis | 0 Interaction, 1 Creation, 2 LastRead | | TagMatchMode | 1 Any, 2 All | | NotificationType | 1 Regular, 2 SilentPush, 3 NoPush | | BroadcastStatus | 1 Waiting, 2 Processing, 3 Completed, 4 Paused, 5 Failed, 6 Cancelled | | BroadcastExecutionStatus | 0 Waiting, 1 Sent, 2 Failed, 3 Processing, 4 Completed, 5 PendingSegmentation (plus Cancelled) | | JourneyStatus | 1 Draft, 2 Active, 3 Paused, 4 Archived, 5 Completed | | JourneyMode | 1 Individual, 2 Synchronized, 3 IndividualTimeSlot | | JourneyStepSelectionMode | 1 Single, 2 RandomFromSet | --- ## 4. Pages (channels) | Method | Path | Notes | |---|---|---| | GET | `/v1/pages` | List of the caller's pages with filters (`search`, `provider`, `categoryIds`, `statuses`, `countries`, `hasSchema`, `hasOpenIssues`, `isTokenExpired`, ...), `limit`, `cursor`, `sort` (`name`, `createdAt`, `updatedAt`, `leadCount`, `status`, `category`, `country`, `account`, `company`, `leadsToday`, `leads7d`, and `firstReply`, `defaultReply`, `sequence` for the name of the flow that answers in that role). `?fields=` picks the columns to fill (`connectedApps`, `issues`, `schemas`, `urls`, `leads`, `integrity`); `?fields=connectedApps` gives a fast, light list. Start here. | | GET | `/v1/pages/facets`, `/v1/pages/export` | Counts per filter value; CSV of the filtered list. | | GET | `/v1/pages/{pageId}` | One page with `profileCounts`, `connectedApps`, `issues`, `schemas`. | | PATCH | `/v1/pages/{pageId}` | `{"timeZone", "language", "currency", "countryCode", "categoryId", "categoryVariableGroupId"}` (all optional; `""` clears a text, an empty GUID clears the category or the group). `PATCH /v1/pages` with `{"pageIds", "settings"}` applies the same settings to up to 100 pages. | | DELETE | `/v1/pages/{pageId}` | Soft delete. `POST /v1/pages/{pageId}/reactivate` brings it back. Batches: `DELETE /v1/pages?ids=`, `POST /v1/pages/reactivate {"pageIds"}`. | | GET | `/v1/pages/{pageId}/health`, `POST /v1/pages/{pageId}/webhook` | Checks the page setup at the provider (webhook subscription, Messenger profile) and redoes it. | | GET | `/v1/pages/{pageId}/integrity`, `/v1/pages/{pageId}/url-health` | Meta integrity status (violations, restrictions, appeal link); health of the URLs used in buttons. | | GET | `/v1/connections`, `/v1/connections/{connectionId}`, `/v1/pages/{pageId}/connections` | Connection history: who connected, by which app, the result and the pages touched. | | POST | `/v1/meta/pages?accountId=` | Connect Facebook/Instagram pages. Needs a Meta USER access token from Facebook Login in a browser (`{"facebookId", "userAccessToken", "metaAppId", "permissions"}`), answers `202 {"connectionId", "waiting", "message"}`. An agent cannot complete this without a human in a browser; ask the user to connect the page in the web app. | | POST | `/v1/telegram/pages?accountId=` | `{"botToken": ""}` answers `201` with the page. Fully automatable. `409` if the bot is already connected to another company. | | GET | `/v1/telegram/pages/{pageId}/channels` | Channels and groups the Telegram bot administers: `{"items": [{"channelReferenceId", "title", "username", "status", "canPostMessages", "isActive"}]}`. Use `channelReferenceId` in `channelTargets` of a flow or broadcast. | | GET, PUT | `/v1/telegram/pages/{pageId}/presentation` | Bot description, short description and command list. | | GET, PUT | `/v1/meta/pages/{pageId}/ice-breakers` | Facebook ice breakers (max 4). PUT `{"configured": bool, "entries": [{"keyword", "label"}]}`; `configured: false` derives them from the flows' `messageTriggers` and `triggerLabels`. | | GET, POST, PUT, PATCH, DELETE | `/v1/pages/{pageId}/variables`, `/v1/pages/{pageId}/variables/{variableId}`, `POST /v1/pages/{pageId}/variables/copy {"sourcePageId"}` | Page variables `{"key", "value", "dynamicValue", "type"}` usable as `{{KEY}}` in scripts. `PUT` on the collection upserts by key; `DELETE ?ids=` removes many and answers `{"deleted": n}`; `PATCH` on one variable changes only what you send, and `"dynamicValue": null` clears the dynamic value. Reserved keys (rejected): NAME, FIRST_NAME, EMAIL, PAGE_ID, PAGE_NAME, ORDER_ID, DELIVERY_AT, DATE_NOW, PROFILE_ID, LEAD_ID, PSID, META_AD_ID, CAMPAIGN_AD_ID. | | GET, POST, PATCH, DELETE | `/v1/categories`, `/v1/categories/{categoryId}` | Categories group pages; flows and broadcasts can be filtered by `categoryIds`. | | GET, POST, PATCH, DELETE | `/v1/categories/{categoryId}/variable-groups`, `/v1/variable-groups/{groupId}`, `/v1/variable-groups/{groupId}/variables` | Category-level variables shared by the pages of a category. `PATCH {"isDefault": true}` makes a group the default; `POST /v1/variable-groups/{groupId}/duplicate {"name"}` copies it. | | GET | `/v1/alerts`, `/v1/alerts/summary` | Open page issues (expired tokens, blocks) and the health summary; each page carries `pageReferenceId` and each alert its `tokenId` when it is about a token. `POST /v1/alerts/{alertId}/dismiss`, `POST /v1/alerts/{alertId}/snooze {"until"}`. | Provider capabilities that matter when authoring: | Capability | Facebook | Instagram | Telegram | |---|---|---|---| | Quick replies (`text` node options) | yes (max 13) | yes | rendered as inline buttons | | Button template (`buttons_menu`, 1 to 3 buttons) | yes | yes | yes | | Carousel (`simple_menu`, 1 to 10 cards) | yes | yes | no | | `location` request | yes | no | yes | | `share_feed`, `recurring` (notification opt-in) | yes | no | no | | `utility_template` (Meta utility templates, escape 24h window) | yes | yes | no | | `handover_protocol` | yes | yes | no | | Delivery and read receipts (metrics `delivered`/`reads`) | yes | yes | no (platform limitation) | | Broadcast outside the 24h window | via MessageTag, UtilityTemplate or Automatic | same | always (`type` 6 All) | | Button colour `style` | ignored | ignored | `primary`/`success`/`danger` | --- ## 5. Flows (schemas): lifecycle ### 5.1 Flow body ```json { "name": "string (required)", "provider": 1, "status": 0, "channelTrigger": 0, "pageIds": [""], "messageTriggers": ["PROMO", "OFERTA"], "triggerMatchType": 0, "triggerLabels": {"PROMO": "Ver promoções"}, "unsubscribeMessageTriggers": ["SAIR", "PARAR"], "unsubscribeTriggerMatchType": 0, "script": "", "trackLinkClicks": true, "dynamicUpdate": {"enabled": false}, "channelTargets": [{"pageId": "", "includeProfiles": true, "channelReferenceIds": ["-100123"]}], "translation": {"enable": true, "languages": []}, "conversationSettings": { "whenUnmatched": {"mode": "restartWelcome", "route": null, "schemaId": null, "onEnd": null}, "whenFinished": {"mode": "restartWelcome"}, "whenAlreadyScheduled": "skip", "escapeKeywords": ["MENU"] } } ``` The response adds `id`, `accountId`, `pages` (`[{"id", "name", "referenceId", "providerId", "categoryId", "status", "language"}]`, with the page's current state), `totalPages`, `categories`, `variableValidation`, `createdAt` and `updatedAt`. - `pageIds` links the flow to pages. A flow only answers inbound messages (Inbox, FirstReply, keywords) on pages it is linked to. Broadcasts and journeys take their own `pageIds`, but the flow must still match the page's provider. - `channelTrigger` decides when the flow is eligible. For a keyword-triggered flow use `0 Inbox` (or `-1 All`) plus `messageTriggers`. For a "first contact" welcome use `4 FirstReply`. A flow that will only be broadcast can use `2 Broadcast`. - `messageTriggers` are compared (`triggerMatchType` 0 exact, 1 contains, case-insensitive) with the lead's text by Flow.API, which then serves the matching flow to the bot. Keyword resolution is NOT inside the script. - `unsubscribeMessageTriggers` mark the lead as unsubscribed (removed from broadcasts) when matched. - `conversationSettings` live on the flow, never inside `script` (every top-level script key is a route; a settings key there becomes a phantom route that the visual editor deletes). Modes for `whenUnmatched.mode`: `restartWelcome` (default), `route` (needs `route`), `goToFlow` (needs `schemaId`), `repeat`, `stay`, `advance` (`onEnd`: `restart`|`stop`|`goToFlow`), `llmIntent` (not active; degrades to default). `whenAlreadyScheduled`: `skip`|`replace`|`allow`. `escapeKeywords` break a lead out of an open `awaitReply` question. - `translation.enable` with `languages: ["en", "es"]` asks the platform to produce translated variants; the source language is detected server-side. ### 5.2 Endpoints | Method | Path | Notes | |---|---|---| | POST | `/v1/schemas?accountId=` | Body as in §5.1. `201` with the created flow (with `id`). Runs script validation (§6.9). | | PUT | `/v1/schemas/{schemaId}` | Full replace. Send the whole flow, including `script`; send `translation` back as you read it (omitted, it is cleared). | | PATCH | `/v1/schemas/{schemaId}` | `{"status": 1}` publishes, `{"status": 0}` goes back to draft. | | GET | `/v1/schemas/{schemaId}` | Full flow including `script` and `variableValidation`. | | GET | `/v1/schemas?name=&status=1&pageIds=&provider=&channelTrigger=&categoryIds=&limit=&cursor=` | Newest first, `limit` up to 100. `GET /v1/schemas/category-counts` gives how many flows each category has under the same filters. | | POST | `/v1/schemas/{schemaId}/copy` | `{"name": "string", "copyPages": bool, "copyFlow": bool}` answers `201` with the copy. | | POST | `/v1/schemas/validate-variables` | `{"script", "pageIds"}` (dry run). Returns `{"status", "totalPages", "pagesWithProblems", "problems": [{"pageId", "pageName", "referenceId", "providerId", "categoryId", "status", "language", "missingVariables": []}]}`: which pages lack a value for a `{{VARIABLE}}` the script uses. | | DELETE | `/v1/schemas/{schemaId}` | `400` if a Waiting broadcast or a journey step uses it; soft-deletes when it has history. | | GET | `/v1/schemas/{schemaId}/metrics?broadcastId=` | Per-node funnel for one broadcast (`metrics:read`). `GET /v1/schemas/{schemaId}/metrics/broadcasts` lists the broadcasts of the flow. | | GET | `/v1/schemas/{schemaId}/route-weight-suggestion` | Latest suggestion of weights for the flow's A/B links. | | POST | `/v1/schemas/{schemaId}/test` | `{"pageId": "", "leadId": ""}`. Sends the flow to ONE existing lead of that page right now, bypassing triggers (`202`). The lead must already exist (has talked to the page). Use it to smoke-test a flow before broadcasting. | | GET | `/v1/schemas/{schemaId}/utility-templates` | Status of the Meta utility templates generated for `utility_template` nodes: `{"items": [{"templateId", "status", "templateName", "languageCode", ...}]}`. Only `Approved` templates are sent; poll after publishing. `POST /v1/utility-templates/{templateId}/retry {"pageId"}` asks for a new submission for one page, `POST /v1/utility-templates/retry` for many. | | GET | `/v1/meta/pages/{pageId}/templates`, `/v1/meta/template-library` | Message templates of the page read from Meta, and Meta's template library. Both page with `limit` and Meta's own cursor (`nextCursor` only when the page came full). | | GET, POST | `/v1/tags?accountId=` | Tag names of the account (`{"name"}` to create). Tags are also created implicitly by `addTag` nodes and by segmentation. | ### 5.3 Typical sequence 1. `GET /v1/accounts` to pick `accountId`. `GET /v1/pages?fields=connectedApps` to pick page ids of one provider. 2. Build the script (§6). Keep route names UPPER_SNAKE and short. 3. `POST /v1/schemas?accountId=...` with `status: 0`, `pageIds`, `channelTrigger`, `messageTriggers`, `script`. 4. Optionally `POST /v1/schemas/validate-variables` and fix page variables. 5. `PATCH /v1/schemas/{schemaId}` with `{"status": 1}` to publish. If the script has `utility_template` nodes, poll `GET /v1/schemas/{schemaId}/utility-templates` until the templates are `Approved` (Meta review can take minutes to hours). 6. `POST /v1/schemas/{schemaId}/test` to yourself, then `POST /v1/broadcasts` (§7) or let inbound triggers do the work. --- ## 6. The flow script contract (what the bot executes) `script` is a JSON document serialized as a string inside the flow's `script`. Escape it accordingly (`"script": "{\"WELCOME\": ...}"`). ### 6.1 Routes ```json { "WELCOME": [ , ], "ROUTE_2": { "MESSAGES": [ ], "messageTriggers": ["X"], "_metadata": { ... } } } ``` - Every top-level key is a ROUTE. Never put configuration at the top level. - A route is either an array of nodes or an object whose `MESSAGES` array holds the nodes (the visual editor writes the object form and adds `_metadata` with node coordinates; both forms are accepted). Prefer the array form when generating. - `WELCOME` is REQUIRED: it is the entry route for inbound messages and for broadcasts without `startRoute`, and the fallback for any missing route. - Nodes run in order. A node's `redirect` jumps to another route (position 0) after the node runs. When a route ends without `redirect`, the conversation stops there. - Route names are case-sensitive strings. Keep them short (Telegram callback data is limited to 64 bytes and carries `route~token`). - A one-time-notification opt-in (`recurring` node with `topic: "X"`) routes the opt-in event to a route named `X_SUCCESS`: define it. ### 6.2 Node shape Common fields: | Field | Type | Meaning | |---|---|---| | `id` | string | Unique node id (the editor uses `n_` + 12 hex chars). Strongly recommended: metrics, link tracking and waits are keyed by it. Flow.API fills missing ids on save. | | `format` | `"message"` \| `"action"` | REQUIRED as `"action"` for every action node (§6.4). Omitted or anything else = message. | | `type` | string | Node type (§6.3, §6.4). | | `redirect` | route name | "Next" connector, applied after the node. | | `redirectFallback` | route name | "Fallback" connector: in broadcasts, when Meta rejects the message because the 24h window is closed (or a button title is empty), the lead is re-dispatched to this route (max 3 hops). Also the entry for utility-template sequences. | | `awaitReply` | object | Pause and capture the lead's next message (§6.6). | | `message` | object | `{"text", "image", "media", "media_format", "option": [buttons]}` depending on the type. | | `option` / `options` | array | Buttons, carousel cards or random branches. | | `title` | string | Body text of the button template, card title or recurring title. | ### 6.3 Message nodes (`format: "message"`) `text`: text with optional quick replies (max 13; titles up to 20 chars; text up to 2000). ```json {"id": "n1", "type": "text", "message": {"text": "Olá {{FIRST_NAME}}! Quer ver o cardápio?", "option": [{"id": "b1", "type": "redirect", "title": "Sim", "action": "CARDAPIO"}, {"id": "b2", "type": "url", "title": "Site", "url": "https://example.com"}]}, "redirect": "PROXIMO"} ``` `buttons_menu`: Meta button template (1 to 3 buttons, text up to 640) or, with `message.image`, a single generic-template card (`title` up to 80, `message.text` as subtitle up to 80, `media_format`: `square`|`horizontal`). Legacy alias `button`. ```json {"id": "n2", "type": "buttons_menu", "title": "O que deseja?", "message": {"image": "https://cdn/x.png", "text": "Escolha uma opção", "media_format": "square"}, "option": [{"id": "b1", "type": "redirect", "title": "Pedidos", "action": "PEDIDOS"}, {"id": "b2", "type": "url", "title": "Nosso site", "urls": [{"url": "https://a.com", "weight": 50}, {"url": "https://b.com", "weight": 50}]}]} ``` `simple_menu`: carousel, 1 to 10 cards; each card `{"id", "title" (up to 80), "subtitle" (up to 80), "image", "media_format", "default_action": true, "url", "option": [up to 3 buttons]}`. Not available on Telegram. `attachment`: `{"type": "attachment", "option": [{"media": "image"|"video"|"audio"|"file", "url": "https://...", "caption": "Telegram only, up to 1024"}]}`. Only `option[0]` is used. Upload the file first with `POST /v1/uploads` (§9); for a Telegram voice message, convert the audio with `POST /v1/voice-messages`. `location`: asks for the lead's location: `{"type": "location", "message": {"text": "Compartilhe sua localização"}}` (Facebook, Telegram). `typing`: `{"type": "typing"}` shows the typing indicator. `share_feed` (Facebook): generic card with a share button: `option: [{"title", "subtitle", "image", "share": {"title", "url"}}]`. `recurring` (Facebook): notification-messages opt-in: `{"type": "recurring", "title": "Receba ofertas (up to 65)", "topic": "OFERTAS", "frequency": "DAILY"|"WEEKLY"|"MONTHLY", "message": {"image": "..."}}`. Define route `OFERTAS_SUCCESS`. `utility_template` (Meta): sends a pre-approved utility template, escaping the 24h window. `templateId` is required and refers to a template Flow.API generates and registers from the node content after publish; the node never degrades to a plain message. ```json {"id": "n8", "type": "utility_template", "templateId": "", "title": "Cabeçalho", "message": {"text": "Corpo", "image": "https://cdn/x.png", "option": [{"id": "b1", "type": "redirect", "title": "Ver", "action": "ROUTE_2"}, {"id": "b2", "type": "url", "title": "Site", "urls": [{"url": "https://a.com/x", "weight": 100}]}]}, "redirect": "ROUTE_2", "redirectFallback": "ROUTE_3"} ``` When generating a new flow, create the node with `template: true`, `headerType: "text"` and without `templateId`; Flow.API assigns `templateId` on save. Poll `GET /v1/schemas/{schemaId}/utility-templates` for approval. ### 6.4 Action nodes (`format: "action"`) | `type` | Fields | Behaviour | |---|---|---| | `delay` | `seconds` (int; default 600 if absent), `redirect` | Up to 10 s waits inline; longer delays are scheduled and the flow resumes later at `redirect` (or the next node). Scheduled resumes are sent as `POST_PURCHASE_UPDATE`-tagged messages when outside the 24h window. | | `goTo` | `schemaId` (guid, required), `route` (default `WELCOME`) | Switch to another flow. | | `random` | `options: [{"action": "ROUTE_A"}, {"action": "ROUTE_B"}]` | Uniform random branch. | | `hasTagConditional` | `conditionType`: `"tag"` (default; needs `tag`), `"subscription"` (`subscriptionTag`, default `DEFAULT`), `"hasValue"` (`conditionField`); `redirectTrue`, `redirectFalse` | Branch. `subscription` reads `SUBSCRIPTION_{SLUG}_ACTIVE`/`_EXPIRES_AT` from the lead. `hasValue` checks a captured `PROVIDED_`. | | `addTag` / `removeTag` | `tag` (string) | Applies to the lead asynchronously (visible to the next message within about 2 minutes via a snapshot). | | `unsubscribe` | none | Marks the lead unsubscribed and stops. | | `handover_protocol` | none | Passes the thread to the page inbox or a human agent (Meta only). | | `pix` | `valueCents` (positive int, required), `comment`, `expiresInSeconds`, `subscriptionPeriod`: `lifetime`|`daily`|`weekly`|`monthly`|`quarterly`|`annual`, `subscriptionTag`, `testMode`, `redirect` (next), `redirectTrue` (route resumed after payment, the "Pagou" connector) | Generates a Pix charge, delivers the QR and BR Code, and on payment marks `SUBSCRIPTION_{TAG}_ACTIVE` and resumes at `redirectTrue`. Requires the account to have Pix enabled. | Example conditional: ```json {"id": "c1", "format": "action", "type": "hasTagConditional", "conditionType": "tag", "tag": "CLIENTE", "redirectTrue": "JA_CLIENTE", "redirectFalse": "NOVO"} ``` ### 6.5 Buttons ```json {"id": "b1", "type": "redirect"|"url", "title": "up to 20 chars (64 on Telegram)", "action": "ROUTE_NAME", // for redirect: target route "url": "https://...", // for url: single link, or "urls": [{"url": "https://...", "weight": 60, "directoryId": "...", "domainId": "..."}], // weighted A/B links (weights sum up to 100) "style": "primary"|"success"|"danger"} // Telegram only, ignored elsewhere ``` - `title` is REQUIRED; blank titles drop the button; an empty title in a utility template makes Meta reject the message (routes to `redirectFallback`). - Any `type` other than `"url"` is a postback. Postback payloads are built by the bot as `{"route", "schemaId", "broadcastId", "nodeId", "buttonId", "channel"}`; you never author payloads. - `useWebsite: true` with `websiteCategoryIds` resolves the URL from the account's website pool (`/v1/website-domains`) at send time. - URL buttons are wrapped for click tracking when the flow has `trackLinkClicks` (default true). ### 6.6 Capturing answers: `awaitReply` Any message node can wait for the lead's reply: ```json {"id": "q1", "type": "text", "message": {"text": "Qual é a sua cidade?"}, "awaitReply": {"enabled": true, "saveAs": "CITY", "expects": "text", "onReply": "next", "onInvalid": {"route": "NAO_ENTENDI"}, "timeout": {"seconds": 3600, "route": "SEM_RESPOSTA"}, "oneTouch": false, "sensitive": false}} ``` - `enabled` must be literally `true`. `saveAs` (required by publish validation) matches `^[A-Za-z_][A-Za-z0-9_]*$` and must not be a reserved name (`NAME, FIRST_NAME, EMAIL, ORDER_ID, DATE_NOW, DELIVERY_AT, PROFILE_ID, LEAD_ID, PSID`). The value is stored on the lead as `PROVIDED_CITY` and is available in later nodes as `{{CITY}}`. - `expects`: `any` (default), `text`, `number`, `email`, `phone`, `cpf`, `date`, `attachment`, `location`. Values are canonicalized (E.164 phone, lower-case email, digits-only CPF with check digits, ISO date). `cpf`, `email` and `phone` are treated as sensitive (no raw copy stored) unless `sensitive: false`. - `onReply`: `"next"` continues after the question; a route name jumps there. `onInvalid.route` runs on a type mismatch (no automatic re-ask; omit to just continue). `timeout` defaults to 300 s and only fires when `timeout.route` is set; `"timeout": null` disables. - `oneTouch: true` adds the provider's one-tap share (phone or email quick reply on Meta, contact or location keyboard on Telegram) for `expects` phone, email or location. - In broadcasts the wait opens when Meta ACCEPTS the message, not when it is queued. ### 6.7 Variables Syntax `{{NAME}}` (also `{{$NAME}}`), `[A-Za-z0-9_]` only, case-insensitive, unknown placeholders are left as they are. Built-ins: `NAME`, `FIRST_NAME`, `EMAIL`, `ORDER_ID` (random 6 digits), `DATE_NOW` (UTC `YYYY-MM-DD`), `DELIVERY_AT` (today + 3 days), `PROFILE_ID`/`LEAD_ID`/`PSID` (the lead's provider id). Page and category variables by `key`. Captured answers by their `saveAs` name (and `{{CITY_RAW}}` for the raw text). Per-flow dynamic ad keys resolve to the ad id that brought the lead. ### 6.8 Limits (Meta unless noted) text 2000 · button-template text 640 · button title 20 (Telegram 64) · quick replies 13 · buttons per template 3 · carousel cards 10 · card title and subtitle 80 · recurring title 65 · Telegram text 4096 · Telegram caption 1024 · Telegram callback data 64 bytes (route name + 8) · broadcast bundle 50 messages per lead · flow depth 100 nodes per message. ### 6.9 Publish-time validation (Flow.API) `POST /v1/schemas` and `PUT /v1/schemas/{schemaId}` reject the script with `400` when: an `awaitReply.enabled` node lacks a valid, non-reserved `saveAs`; `expects` is not one of the allowed values; any `style` is not `primary|success|danger`. Flow.API also normalizes `messageTriggers`, assigns missing node ids and `templateId`s, and checks that linked pages have values for the variables used (`variableValidation`). The bot itself is lenient: over-long texts are truncated, invalid buttons dropped, unknown node types skipped with a metric. ### 6.10 Checklist for a valid script 1. Top level = routes only, includes `WELCOME`. 2. Every node has a unique `id`; action nodes have `format: "action"`. 3. Every `redirect`, `redirectTrue`, `redirectFalse`, `redirectFallback`, `option[].action`, `options[].action`, `awaitReply.*.route` names an existing route. 4. Button templates 1 to 3 buttons with non-blank titles; carousels 1 to 10 cards; quick replies up to 13. 5. URL buttons use absolute http(s) URLs. 6. `utility_template` nodes are only used on Meta providers and get their `templateId` from Flow.API. 7. No node type outside the provider matrix (§4). Minimal complete example (Facebook, keyword flow): ```json { "WELCOME": [ {"id": "n1", "type": "text", "message": {"text": "Oi {{FIRST_NAME}}! Posso ajudar?", "option": [{"id": "b1", "type": "redirect", "title": "Cardápio", "action": "CARDAPIO"}, {"id": "b2", "type": "redirect", "title": "Falar com humano", "action": "HUMANO"}]}} ], "CARDAPIO": [ {"id": "n2", "format": "action", "type": "addTag", "tag": "INTERESSE_CARDAPIO"}, {"id": "n3", "type": "buttons_menu", "title": "Veja o cardápio completo", "option": [{"id": "b3", "type": "url", "title": "Abrir", "url": "https://example.com/menu"}]} ], "HUMANO": [ {"id": "n4", "type": "text", "message": {"text": "Um atendente vai falar com você."}}, {"id": "n5", "format": "action", "type": "handover_protocol"} ] } ``` --- ## 7. Broadcasts ### 7.1 Create ``` POST /v1/broadcasts { "schemaId": "", "pageIds": ["", "..."], "type": 5, "messageTag": null, "notificationType": 1, "recencyFilter": 0, "recencyBasis": 0, "recencyOlderThanHours": null, "recencyNewerThanHours": null, "tags": ["VIP"], "tagMatchMode": 1, "onlySubscribers": false, "subscriptionTag": null, "scheduledTo": "2026-09-10T09:00:00", "isImmediate": false, "channelTargets": [{"pageId": "", "includeProfiles": true, "channelReferenceIds": ["-1001234"]}] } → 201 broadcast (+ "skippedPageIds": [...] when some pages were dropped) ``` Rules enforced server-side: - `schemaId` must be `Published` (`400 "Schema is not published"`). `pageIds` non-empty; pages must be of the flow's provider. - `type`: on Meta pages prefer `5 Automatic`: the bot cascades per lead through the channels that can deliver (utility template, then message tag, then standard 24h). `1 Standard` only reaches leads who wrote in the last 24h. `3 MessageTag` requires `messageTag` and Meta policy compliance. `4 UtilityTemplate` requires approved templates in the flow. `6 All` is Telegram only (`400` otherwise). - Recency (`recencyFilter` other than 0) is honoured only for types 3, 4 and 5 (silently reset to All otherwise). `5 Custom` needs at least one of `recencyOlderThanHours`/`recencyNewerThanHours`, each in 1..2160 hours, older lower than newer. - `scheduledTo` is interpreted as the LOCAL wall clock of each page (`timeZone`), so one broadcast to pages in several zones produces one execution per zone. Omit it (or set `isImmediate: true`) to send now. - `tags` + `tagMatchMode` (1 any, 2 all) filter the audience by lead tags. `onlySubscribers` restricts to leads with an active Pix subscription (`subscriptionTag` narrows to one). - Unsubscribed and blocked leads are always excluded; the bot also skips leads in temporary cooldown. - `POST /v1/broadcasts/delivery-preview` (`broadcasts:read`) estimates, before sending, how the delivery rules would treat the audience of the pages. ### 7.2 Monitor and control | Method | Path | Notes | |---|---|---| | GET | `/v1/broadcasts/{broadcastId}` | Summary: `status`, `type`, `scheduledTo`, `totalPages`, `executionSummary`, `metricsSummary`, `pages` (`pageId`, `pageReferenceId`, `name`, category). | | GET | `/v1/broadcasts/{broadcastId}/executions?status=&pageIds=&hasErrors=&limit=&cursor=` | One row per page: `executionId`, `pageId`, `pageReferenceId`, `status`, `total`, `successCount`, `failureCount`, `pendingCount`, `sent`, `errorCount`, `topError`, `isBlocked`, `blockedReason`. | | GET | `/v1/broadcasts?status=&pageIds=&dateFrom=&dateTo=&search=&journeyId=&limit=&cursor=&sort=` | List of broadcasts, with `total` and `categoryCounts`. `GET /v1/broadcasts/groups?groupBy=hour|categoryHour&timezoneOffset=-03:00` returns grouped counts instead; `GET /v1/broadcasts/calendar?dateFrom=&dateTo=` the calendar view. | | GET | `/v1/broadcasts/overview?days=7` | `{"broadcasts": {"total", "scheduled", "inProgress"}, "executions": {"success", "inProgress", "withErrors", "blocked"}, "leads": {"planned", "success", "sent", "errors"}}`. | | GET | `/v1/broadcasts/executions?schemaId=&broadcastId=&pageIds=&dateFrom=&dateTo=&status=&limit=` | Global execution feed (defaults to today). | | PUT | `/v1/broadcasts/{broadcastId}` | Replace a Waiting or Paused broadcast with the same body and checks as the creation; an optional field left out is cleared. | | PATCH | `/v1/broadcasts/{broadcastId}` | Change part of a Waiting or Paused broadcast: a missing field is kept, `null` clears `topicName`, `messageTag`, the recency limits, `tags`, `deliveryIntelligence`, `subscriptionTag`, `scheduledTo` or `channelTargets`, and `null` on any other field is a `400`. | | POST | `/v1/broadcasts/{broadcastId}/cancel` | `400` when Completed, Failed or Cancelled. | | POST | `/v1/broadcasts/{broadcastId}/executions/{executionId}/cancel`, `/reprocess`; `/v1/broadcasts/{broadcastId}/executions/cancel`, `/reprocess` with `{"pageId"}` | Per-page control. Reprocess only for failed executions within 24h (`409` otherwise). | | GET | `/v1/schemas/{schemaId}/metrics?broadcastId=` | Per-node sent, delivered, read, click and error counts for that broadcast. | Delivery happens asynchronously: an execution goes `PendingSegmentation`, then `Processing`, then `Completed`; leads are sent in batches and the bot paces per page to respect Meta's per-page rate limit (error 613), retrying for up to 24h. Expect `status` to move over minutes to hours for large audiences. --- ## 8. Journeys (sequences) ``` POST /v1/journeys?accountId= { "name": "Onboarding", "mode": 1, "provider": 1, "repeatFromStart": false, "cycleGapHours": null, "entryDelayHours": 24, "pageIds": [""], "categoryIds": [], "steps": [ {"index": 0, "dayOffset": 0, "timeOfDay": "09:00", "schemaIds": [""], "selectionMode": 1, "broadcastType": 5, "notificationType": 1}, {"index": 1, "dayOffset": 2, "timeOfDay": "10:30", "schemaIds": ["", ""], "schemaWeights": [70, 30], "selectionMode": 2, "broadcastType": 5} ] } → 201 journey (status forced to 1 Draft) ``` - `mode`: 1 Individual (each lead's own clock; `timeOfDay` is a relative wait), 2 Synchronized (one wall clock for everyone), 3 IndividualTimeSlot (per lead, aligned to `timeOfDay`; requires all pages in one timezone). - Every step flow must be Published and match `provider`. Set `broadcastType` explicitly (5 Automatic recommended; 1 Standard strands leads outside the 24h window). - Activate with `PATCH /v1/journeys/{journeyId}/status {"status": 2}` (`204`). Allowed requests: 2 Active, 3 Paused, 4 Archived. Activation needs a name, at least one valid step and at least one page. - `GET /v1/journeys?status=&pageIds=&search=&limit=&cursor=`, `GET /v1/journeys/{journeyId}`, `PUT /v1/journeys/{journeyId}` (full replace), `POST /v1/journeys/{journeyId}/copy {"name", "copyAudience", "copySteps"}`, `GET /v1/journeys/{journeyId}/leads-count` (`{"total"}`), `GET /v1/journeys/{journeyId}/funnel?granularity=step|day&dateFrom=&dateTo=&cycle=`. - `POST /v1/journeys/delivery-preview` (`journeys:read`) estimates how the delivery rules would treat the audience before activation. - Broadcasts created by a journey carry `journeyId`/`journeyStepIndex` and can be filtered with `journeyId` / `hasJourney` on `/v1/broadcasts` and `/v1/broadcasts/executions`. --- ## 9. Leads, tags and media | Method | Path | Notes | |---|---|---| | GET | `/v1/leads?pageIds=&search=&tags=&provider=&sort=-updatedAt&limit=&cursor=` | Newest activity first, `limit` up to 100. Each item: `id`, `name`, `surname`, `username`, `email`, `phone`, `pictureUrl`, `pageId`, `pageName`, `accountId`, `provider`, `externalId`, `properties` (map), `tags`, `createdAt`, `updatedAt`. | | GET | `/v1/leads/{leadId}` | One lead, with `tagEvents`. | | DELETE | `/v1/leads/{leadId}` | User session only (not available to API tokens). `DELETE /v1/pages/{pageId}/leads` (`202`) deletes every lead of a page in the background; follow it at `GET /v1/pages/{pageId}/leads/deletion`. | | GET, POST | `/v1/tags?accountId=` | Account tag names. | | POST | `/v1/uploads?accountId=` | `{"folder": "images", "extension": "png"}` answers `201 {"url"}`, a pre-signed URL: send the file there with `PUT`, then use the URL without the query string in the script. | | POST | `/v1/voice-messages` | `{"url": "