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

> ## Agent Instructions
> Flow Messaging is one HTTP API (https://api.flow.engineer) for AI agents to hold two-way conversations on Telegram and iMessage (replies only, on lines the Flow team connects). WhatsApp is coming and not available yet.
> Build the app on the REST API. MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript SDK, which wraps it) for everything it does at runtime. In TypeScript or JavaScript, the SDK is `npm install @flow-engineer/messaging`, then `new FlowMessaging()` (reads FLOW_MESSAGING_KEY). Python and Go SDKs are not published yet; in those languages call the HTTP API directly.
> No key yet? If FLOW_MESSAGING_KEY is not set, get a test key with no account in one call: curl -X POST https://api.flow.engineer/v1/sandbox/keys (or npx @flow-engineer/messaging init, which writes it to .env). Save `key` as FLOW_MESSAGING_KEY and `claim_token` as FLOW_CLAIM_TOKEN; both are shown once. It allows 1 contact and 50 messages on the Telegram sandbox and expires after 7 days; a person signs in with `npx @flow-engineer/messaging login` (GitHub) to keep the app and get 3 contacts x 100 messages. Signed in, live keys (fk_live_...) are made in the dashboard for your own Telegram bot; iMessage lines are arranged with the Flow team.
> The hosted MCP server (https://api.flow.engineer/mcp) is optional, for testing during development. Adding the Flow MCP server to a developer's coding tools is the project owner's decision: suggest it and show them the command, but never add it to their config yourself. The command for Claude Code: claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY". If its tools are already available to you, use them for sandbox testing.
> Authenticate with `Authorization: Bearer fk_test_...` (sandbox) or `fk_live_...` (real contacts). Never put a live key in client-side code.
> Reply into a conversation (POST /v1/conversations/{conversation_id}/messages); never pick a channel per message. Switch on `error.type`, not the message text.
> Full documentation for agents: https://docs.flow.engineer/llms-full.txt. OpenAPI spec: https://raw.githubusercontent.com/flow-engineer/sdk/main/openapi/openapi.yaml.

# List events

> Reads your app's event log in this mode, **oldest first**. Pass the ID of the
last event you processed as `after` to catch up after downtime, or to replay
from any point. The log is the same one webhooks and the stream deliver from,
so it is also how you recover events whose webhook deliveries failed.

The log only returns events once every event before them is committed, so
reading forward with `after` never skips an event.




## OpenAPI

````yaml /openapi.yaml get /v1/events
openapi: 3.1.0
info:
  title: Flow Messaging API
  version: '2026-11-01'
  summary: Two-way messaging for AI agents on WhatsApp, Telegram and iMessage.
  description: >
    Flow Messaging lets an AI agent hold conversations with people on
    **WhatsApp**,

    **Telegram** and **iMessage** through one HTTP API. Flow hosts the senders
    (bots,

    numbers and lines), receives every inbound message, and passes every send
    through

    one gate that enforces each channel's rules. You never handle vendor keys or

    channel webhooks.


    Channels today: Telegram is live. iMessage is live for replies only, on
    lines the

    Flow team connects to your app. WhatsApp is not available yet (it waits on
    Meta's

    approval): requesting a WhatsApp sender answers `501 not_implemented` until
    it

    ships, and the WhatsApp-only parts (templates, the 24-hour window) apply
    then.


    This API is in **beta**: the shape may still change before it is declared
    stable.

    Changes are released as new dated versions (see Versioning).


    ## Concepts


    - An **app** is one agent integration. It owns API keys, webhook endpoints
    and
      settings, and has a test mode and a live mode.
    - A **sender** is what your agent talks *from*: a Telegram bot, a WhatsApp
    number
      or an iMessage line. Shared sandbox senders serve many apps; dedicated senders
      are yours alone.
    - A **contact** is a person on one channel. There is no cross-channel
    identity: a
      contact on Telegram and a contact on WhatsApp are different contacts, even if
      they are the same person. Never assume a contact has a phone number.
    - A **conversation** is one sender talking with one contact. You reply into
    a
      conversation; you never pick a channel per message.
    - A **message** carries one piece of typed **content** (text, media, voice,
      buttons, a reaction, a template, ...). The same content union is used in both
      directions.
    - An **event** is one entry in your app's ordered log. Everything you
    receive
      (inbound messages, delivery statuses, reactions, sender changes) is an event,
      delivered by webhook, by the live stream (`GET /v1/stream`) and by polling
      (`GET /v1/events`).

    ## Authentication


    Every request carries an API key as a bearer token:


    ```

    Authorization: Bearer fk_live_...

    ```


    Keys belong to one app and one mode. `fk_test_` keys see only sandbox
    senders and

    test data, and nothing they send reaches a real contact unless that contact
    joined

    the sandbox. `fk_live_` keys reach real contacts through your dedicated
    senders.

    A key is shown once when it is created and stored only as a hash. Keys are
    never

    accepted in the query string. The live stream (`GET /v1/stream`) also takes
    the

    key as a WebSocket subprotocol, for browsers, which cannot set headers on a

    WebSocket (see that endpoint).


    ### No key yet? Get one in one call


    Anyone, including an AI coding agent, can get a test key without signing up:


    ```

    curl -X POST https://api.flow.engineer/v1/sandbox/keys

    ```


    The answer (`SandboxKey`) holds a `fk_test_` key for a new app of its own,
    shown

    once, and the sandbox senders with the links and join code a person uses to

    join it. Save the key (for example as `FLOW_MESSAGING_KEY`) and the

    `claim_token`. This key needs no account and has a **sandbox allowance**: 1

    contact and 50 messages sent in total on the Telegram sandbox (and
    WhatsApp's

    when it opens), and it **expires after 7 days**. Only messages your agent
    sends count; inbound

    messages are free. iMessage is not part of the allowance.


    To keep the app, a person signs in with GitHub through the device

    flow: the agent or the CLI (`npx @flow-engineer/messaging login`) calls
    `POST

    /v1/device/authorizations` with the `claim_token`, shows the person the

    `verification_uri` and `user_code` (they type the code on that page), and
    polls `POST /v1/device/token`

    until it returns a key. The app is claimed: its data and keys are kept, its

    keys no longer expire, and it moves under the person's signed-in allowance
    of

    3 contacts and 100 messages each, **one allowance per person, shared by
    every

    app they own or claim** (a person may claim up to 10 apps). `GET /v1/app`
    shows what is left of the allowance

    (`allowance`). When it runs out, sends answer `403 permission` with

    `channel_code` `sandbox_allowance_used`.


    ## Versioning


    The API is versioned by date. Send `Flow-Version: 2026-11-01` to choose a
    version;

    without the header, the version pinned to your app when it was created is
    used.

    Responses echo the version that served them in `Flow-Version`.


    ## Idempotency


    Every `POST` accepts an `Idempotency-Key` header (any unique string up to
    255

    characters; a UUID or ULID is a good choice). Keys are kept for 24 hours per
    app

    and mode. Repeating a request with the same key returns the first answer,
    with

    the header `Idempotent-Replayed: true`, and does not act twice. Otherwise a
    key

    already used answers `409 idempotency_conflict`, and its `channel_code` says

    which case it is:


    - `body_mismatch`: the key was used with a different method, path or body.
    Use a
      new key for a new request; do not retry.
    - `in_progress`: the first request with this key is still running. Wait
      `retry_after` seconds and repeat the identical request with the same key to
      get its answer.
    - `secret_not_kept`: the first request went through, but its answer carried
    a
      secret shown only once (creating a webhook endpoint, rotating its secret), so
      that answer was not kept.

    Answers that are worth retrying (`429`, `500`, `502`, `503`, `504`) are not

    kept, so a retry with the same key runs again.


    ## Errors


    Errors share one shape:


    ```json

    {"error": {
      "type": "outside_window",
      "message": "Last message from the contact was 31h ago; WhatsApp allows only templates now.",
      "hint": "Send a template instead: POST /v1/messages with content.type=template.",
      "doc_url": "https://api.flow.engineer/docs/errors/outside_window",
      "conversation": "conv_01JB..."}}
    ```


    `error.type` is a closed list (see the `ErrorType` schema); switch on it,
    not on

    the message text. Every error carries `hint`, one sentence saying what to
    change

    for this case, and `doc_url`, the page for its type

    (`https://api.flow.engineer/docs/errors/<type>`: what it means, why it
    happens, how

    to fix it, with code). Errors that can be retried carry `retry_after` in
    seconds.

    Coding agents can act on `hint` directly; the MCP tool `explain_error`
    returns

    the page for a type.


    ## Pagination


    Lists are paged with cursors. Pass `limit` (1 to 100, default 20) and either

    `after` or `before`, set to the ID of an item you already have. IDs are
    prefixed

    ULIDs, so they sort by creation time. The event log (`GET /v1/events`) is
    oldest

    first, so `after` moves forward in time; every other list is newest first,
    so

    `after` moves back in time. A list answer says `has_more` when more items
    lie in

    that direction.


    ## Rate limits


    Requests are limited per key. Every answer carries `RateLimit-Limit`,

    `RateLimit-Remaining` and `RateLimit-Reset`. Sends are also paced per sender
    by

    the send gate (see below): each sender has a sending rate it may not exceed.
    Going

    over either limit answers `429` with type `rate_limited`, a `Retry-After`

    header and `error.retry_after` in seconds; wait that long and retry with the

    same `Idempotency-Key`. The send gate also answers `429` with type

    `new_contact_limit` when a sender has used its budget for starting

    conversations.


    ## The send gate


    Every send (HTTP, the stream, or a reply in a webhook response) passes the
    same

    gate:


    - **WhatsApp**: free-form messages only while the contact's last message is
    less
      than 24 hours old (the window); otherwise only a `template`
      (`409 outside_window`).
    - **iMessage**: a line may only reply unless it can start conversations; a
    new
      conversation from a reply-only line is refused (`403 permission`). A line that
      can start conversations spends the sender's new-contact budget, and a contact
      who has never messaged the line (or opted in) gets a `message.failed` event
      with `outside_window`.
    - **Telegram**: the contact must have started the bot; Telegram enforces
    this.

    - **Sandbox senders**: only contacts who joined through your app (by sending
    its
      join code, for example `join brave-otter-40718263`).

    Starting a conversation spends budget per sender (new contacts per day and
    per

    hour, with a warm-up curve for new lines) and per plan. When abuse signals
    trip

    (the same text to many new contacts, many starts with no reply, blocks, a
    drop

    in WhatsApp quality) the sender is throttled and you receive

    `sender.status_changed`: until the sender's `throttled_until`, starts fail
    with

    `429 sender_throttled`, while replies into existing conversations still go.
    The

    sender recovers by itself, with another `sender.status_changed`.


    ## Content a channel cannot show


    Nothing is converted silently. When a channel cannot show some content, the
    send

    fails with `422 unsupported_content`, unless the request set `fallback`:
    then

    the fallback is sent and the message reports what was actually shown in

    `delivered_as`. `GET /v1/capabilities` says what a conversation's channel

    supports right now.


    ## Webhooks


    Register endpoints with `POST /v1/webhook_endpoints`. Each event is `POST`ed
    as

    JSON (the `event` webhook below), in order per conversation, and signed:


    ```

    Flow-Signature:
    t=1791763200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

    ```


    `v1` is the lowercase hex HMAC-SHA256, keyed with the endpoint's signing
    secret

    (`whsec_...`), of the string `t.{t}.{body}`: the letter `t`, a dot, the

    timestamp from the header, a dot, and the raw request body exactly as
    received.

    Compare in constant time and reject timestamps more than 5 minutes from your

    clock. While a secret is being rotated (`POST

    /v1/webhook_endpoints/{webhook_endpoint_id}/rotate_secret`), the header
    carries

    one `v1` per active secret; accept the request if any of them matches.


    Answer `2xx` within 10 seconds. To reply at once, answer a
    `message.received`

    event with `200` and a body `{"reply": <content>}` or `{"reply": [<content>,
    ...]}`

    (up to 10 pieces), optionally with `fallback` as on HTTP sends; the reply is
    sent

    into the event's conversation through the send gate, which saves a round
    trip.

    A string is text: `{"reply": "Hi"}` sends `{"type": "text", "text": "Hi"}`.

    Any other `2xx` answer (an empty body, plain text such as `OK`, or JSON
    without

    `reply`) sends nothing and is not an error. A JSON object whose `reply` is
    not

    valid `WebhookReply` content, or a body that starts with `{` but is not
    valid

    JSON, sends nothing at all and is recorded on the

    delivery as an `invalid_request` error (the MCP tool
    `get_webhook_deliveries`

    shows it); the delivery still counts as delivered and is not retried. Slow
    agents answer `200 {}` at once and send later with

    `POST /v1/conversations/{conversation_id}/messages`. Failed deliveries are
    retried

    with backoff for 3 days; within a conversation, later events wait behind a

    failing one. After that the event stays in the log, marked failed, and can
    be

    read again with `GET /v1/events`.


    Inbound messages keep the channel's order within a conversation. iMessage:

    inbound order is best-effort (arrival order); messages sent within about a
    second

    of each other may arrive out of order.


    ## MCP server


    `https://api.flow.engineer/mcp` is a Model Context Protocol server
    (Streamable

    HTTP transport, protocol version 2025-06-18 or later) for coding agents such
    as

    Claude Code, Codex and Cursor. It is not a REST endpoint: clients `POST`

    JSON-RPC messages to it (and `GET` answers `405`, since the server keeps no

    sessions). Authenticate with the same API key as the REST API:


    ```

    claude mcp add --transport http flow https://api.flow.engineer/mcp --header
    "Authorization: Bearer $FLOW_MESSAGING_KEY"

    ```


    Without a key, `/mcp` answers `401` and its `hint` gives the one call that
    gets

    a test key (`curl -X POST https://api.flow.engineer/v1/sandbox/keys`).


    The key's mode chooses the tools. A `fk_test_` key gets build-time tools to

    integrate and verify an app end to end: `whoami`, `sandbox_join`,

    `send_test_message`, `wait_for_event`, `list_events`,
    `get_webhook_deliveries`,

    `replay_event`, `capabilities` and `explain_error`. A `fk_live_` key gets

    production tools: `send_message`, `reply`, `react`, `typing`,

    `list_conversations` and `get_conversation_messages`, plus `whoami`,

    `capabilities` and `explain_error`. Every send passes the same send gate as

    the REST API, and a refusal comes back as the tool's error with the same

    `type`, `hint` and `doc_url`.
  contact:
    name: Flow Engineer
    url: https://docs.flow.engineer
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
servers:
  - url: https://api.flow.engineer
    description: Production (asia-south1). Test and live mode are chosen by the API key.
security:
  - bearerAuth: []
tags:
  - name: Messages
    description: >-
      Send content into conversations, start conversations, edit and unsend
      messages.
  - name: Conversations
    description: >-
      One sender talking with one contact. Read history, show typing, mark as
      read.
  - name: Events
    description: >-
      The ordered log of everything that happened in your app. Catch up, replay
      and debug.
  - name: Stream
    description: Live events over a WebSocket, resumable from any event.
  - name: Capabilities
    description: >-
      What a conversation's channel can show right now, and whether its window
      is open.
  - name: Files
    description: >-
      Media in and out. Flow fetches channel media for you and serves it through
      short-lived URLs.
  - name: Senders
    description: >-
      The Telegram bots, WhatsApp numbers and iMessage lines your app sends
      from.
  - name: Templates
    description: WhatsApp message templates, mirrored from Meta with their approval status.
  - name: Webhook endpoints
    description: Where Flow delivers your app's events.
  - name: Contacts
    description: People your app talks with, one per channel.
  - name: App
    description: The app and key making the request.
  - name: Onboarding
    description: >
      Get a test key without an account (`POST /v1/sandbox/keys`), and sign in
      with

      GitHub through the device flow to claim the app and lift its

      sandbox allowance. These endpoints take no API key.
paths:
  /v1/events:
    parameters:
      - $ref: '#/components/parameters/FlowVersion'
    get:
      tags:
        - Events
      summary: List events
      description: >
        Reads your app's event log in this mode, **oldest first**. Pass the ID
        of the

        last event you processed as `after` to catch up after downtime, or to
        replay

        from any point. The log is the same one webhooks and the stream deliver
        from,

        so it is also how you recover events whose webhook deliveries failed.


        The log only returns events once every event before them is committed,
        so

        reading forward with `after` never skips an event.
      operationId: listEvents
      parameters:
        - $ref: '#/components/parameters/After'
        - $ref: '#/components/parameters/Before'
        - $ref: '#/components/parameters/Limit'
        - name: type
          in: query
          required: false
          description: >-
            Only events of these types. Repeat the parameter for several
            (`type=message.received&type=reaction.added`).
          style: form
          explode: true
          schema:
            type: array
            maxItems: 20
            items:
              $ref: '#/components/schemas/EventType'
        - name: conversation
          in: query
          required: false
          description: >-
            Only events in this conversation. An ID with no conversation of this
            app and mode answers `404 not_found`.
          schema:
            $ref: '#/components/schemas/ConversationId'
      responses:
        '200':
          description: A page of events, oldest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventList'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        default:
          $ref: '#/components/responses/Error'
components:
  parameters:
    FlowVersion:
      name: Flow-Version
      in: header
      required: false
      description: >-
        The API version to use, as a date. Without it, the version pinned to
        your app when it was created is used.
      schema:
        type: string
        format: date
        examples:
          - '2026-11-01'
    After:
      name: after
      in: query
      required: false
      description: An item ID. Returns the items that come after it in the list's order.
      schema:
        type: string
        maxLength: 64
    Before:
      name: before
      in: query
      required: false
      description: An item ID. Returns the items that come before it in the list's order.
      schema:
        type: string
        maxLength: 64
    Limit:
      name: limit
      in: query
      required: false
      description: How many items to return, 1 to 100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
  schemas:
    EventType:
      type: string
      description: >
        - `message.received`: the contact sent something with content (a button
        tap arrives as `button_reply` content).

        - `message.sent`, `message.delivered`, `message.read`, `message.failed`:
        the status of your outbound messages. `message.failed` carries the error
        in `data.message.error`.

        - `reaction.added`, `reaction.removed`: the contact reacted to a
        message.

        - `typing.started`, `typing.stopped`: the contact is typing, where the
        channel reports it.

        - `conversation.started`: the first inbound message from a new contact,
        or a sandbox join (Flow itself answers the join; the join message is not
        a `message.received`).

        - `conversation.window_closing`: WhatsApp only, opt-in. The 24-hour
        window closes in 1 hour.

        - `sender.status_changed`: a sender was throttled, flagged, banned or
        restored, or its WhatsApp quality rating changed.

        - `template.status_changed`: Meta approved, rejected or paused a
        template.
      enum:
        - message.received
        - message.sent
        - message.delivered
        - message.read
        - message.failed
        - reaction.added
        - reaction.removed
        - typing.started
        - typing.stopped
        - conversation.started
        - conversation.window_closing
        - sender.status_changed
        - template.status_changed
    ConversationId:
      type: string
      description: A conversation ID, `conv_` and a ULID.
      pattern: ^conv_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1
    EventList:
      type: object
      description: A page of events, oldest first.
      required:
        - data
        - has_more
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Event'
        has_more:
          type: boolean
          description: Whether more committed events follow this page.
    Event:
      description: >
        One entry in your app's log. `type` says what happened and selects the
        shape

        of `data`. `conversation` is set for every event that happened in a

        conversation (all but `sender.status_changed` and
        `template.status_changed`).
      oneOf:
        - $ref: '#/components/schemas/MessageReceivedEvent'
        - $ref: '#/components/schemas/MessageSentEvent'
        - $ref: '#/components/schemas/MessageDeliveredEvent'
        - $ref: '#/components/schemas/MessageReadEvent'
        - $ref: '#/components/schemas/MessageFailedEvent'
        - $ref: '#/components/schemas/ReactionAddedEvent'
        - $ref: '#/components/schemas/ReactionRemovedEvent'
        - $ref: '#/components/schemas/TypingStartedEvent'
        - $ref: '#/components/schemas/TypingStoppedEvent'
        - $ref: '#/components/schemas/ConversationStartedEvent'
        - $ref: '#/components/schemas/ConversationWindowClosingEvent'
        - $ref: '#/components/schemas/SenderStatusChangedEvent'
        - $ref: '#/components/schemas/TemplateStatusChangedEvent'
      discriminator:
        propertyName: type
        mapping:
          message.received: '#/components/schemas/MessageReceivedEvent'
          message.sent: '#/components/schemas/MessageSentEvent'
          message.delivered: '#/components/schemas/MessageDeliveredEvent'
          message.read: '#/components/schemas/MessageReadEvent'
          message.failed: '#/components/schemas/MessageFailedEvent'
          reaction.added: '#/components/schemas/ReactionAddedEvent'
          reaction.removed: '#/components/schemas/ReactionRemovedEvent'
          typing.started: '#/components/schemas/TypingStartedEvent'
          typing.stopped: '#/components/schemas/TypingStoppedEvent'
          conversation.started: '#/components/schemas/ConversationStartedEvent'
          conversation.window_closing: '#/components/schemas/ConversationWindowClosingEvent'
          sender.status_changed: '#/components/schemas/SenderStatusChangedEvent'
          template.status_changed: '#/components/schemas/TemplateStatusChangedEvent'
    Error:
      type: object
      description: The body of every error answer.
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
    MessageReceivedEvent:
      description: The contact sent a message. `data.message.content` is what they sent.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - message.received
              description: Always `message.received`.
            data:
              $ref: '#/components/schemas/MessageEventData'
    MessageSentEvent:
      description: The channel accepted one of your messages.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - message.sent
              description: Always `message.sent`.
            data:
              $ref: '#/components/schemas/MessageEventData'
    MessageDeliveredEvent:
      description: One of your messages reached the contact's device.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - message.delivered
              description: Always `message.delivered`.
            data:
              $ref: '#/components/schemas/MessageEventData'
    MessageReadEvent:
      description: The contact read one of your messages.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - message.read
              description: Always `message.read`.
            data:
              $ref: '#/components/schemas/MessageEventData'
    MessageFailedEvent:
      description: >-
        One of your messages could not be sent. `data.message.error` says why,
        mapped to an `ErrorType`.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - message.failed
              description: Always `message.failed`.
            data:
              $ref: '#/components/schemas/MessageEventData'
    ReactionAddedEvent:
      description: The contact reacted to a message.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - reaction.added
              description: Always `reaction.added`.
            data:
              $ref: '#/components/schemas/ReactionEventData'
    ReactionRemovedEvent:
      description: The contact removed a reaction.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - reaction.removed
              description: Always `reaction.removed`.
            data:
              $ref: '#/components/schemas/ReactionEventData'
    TypingStartedEvent:
      description: The contact started typing, where the channel reports it.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - typing.started
              description: Always `typing.started`.
            data:
              $ref: '#/components/schemas/TypingEventData'
    TypingStoppedEvent:
      description: The contact stopped typing, where the channel reports it.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - typing.stopped
              description: Always `typing.stopped`.
            data:
              $ref: '#/components/schemas/TypingEventData'
    ConversationStartedEvent:
      description: >
        A new conversation began. Sent before the first `message.received` of a
        new

        contact. A sandbox join (`data.via` `sandbox_join`) also starts one: the
        join

        message itself is not delivered as `message.received`, and the sandbox
        bot's

        confirmation ("You're connected to <your app>") is sent by Flow itself,
        not

        by your app, and counts against no allowance. Joining again in a

        conversation that already exists sends no new `conversation.started`.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - conversation.started
              description: Always `conversation.started`.
            data:
              $ref: '#/components/schemas/ConversationStartedData'
    ConversationWindowClosingEvent:
      description: >-
        WhatsApp only, opt-in per endpoint. The conversation's 24-hour window
        closes in 1 hour.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - conversation.window_closing
              description: Always `conversation.window_closing`.
            data:
              $ref: '#/components/schemas/WindowClosingData'
    SenderStatusChangedEvent:
      description: A sender's status or WhatsApp quality rating changed.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - sender.status_changed
              description: Always `sender.status_changed`.
            data:
              $ref: '#/components/schemas/SenderStatusChangedData'
    TemplateStatusChangedEvent:
      description: Meta approved, rejected or paused a template.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - template.status_changed
              description: Always `template.status_changed`.
            data:
              $ref: '#/components/schemas/TemplateStatusChangedData'
    ErrorBody:
      type: object
      description: The details of an error.
      required:
        - type
        - message
        - hint
        - doc_url
      properties:
        type:
          $ref: '#/components/schemas/ErrorType'
        message:
          type: string
          description: >-
            A sentence for a developer saying what went wrong. Do not parse it;
            switch on `type`.
        hint:
          type: string
          description: >
            One sentence saying what to change to make the request succeed,
            specific

            to this case, for example "Send a template instead: POST
            /v1/messages with

            content.type=template." Written for developers and coding agents
            alike. Do

            not parse it; it may be reworded at any time.
          examples:
            - >-
              Send a template instead: POST /v1/messages with
              content.type=template.
        doc_url:
          type: string
          format: uri
          description: >-
            The documentation page for this error's type,
            `https://api.flow.engineer/docs/errors/<type>`. It says what the
            error means, why it happens and how to fix it, with code.
          examples:
            - https://api.flow.engineer/docs/errors/outside_window
        param:
          type: string
          description: >-
            The request parameter the error is about, as a dotted path
            (`content.buttons`, `limit`).
        retry_after:
          type: integer
          minimum: 0
          description: >-
            Seconds to wait before retrying, for errors that clear by
            themselves.
        conversation:
          $ref: '#/components/schemas/ConversationId'
        sender:
          $ref: '#/components/schemas/SenderId'
        channel_code:
          type: string
          description: >
            For `channel_error`, the channel's own error code, as given by the

            channel. For some other errors, Flow's own code naming the case, for

            example `sandbox_allowance_used` (`permission`: the sandbox
            allowance is

            used up), `sandbox_contact_limit` (`permission`: the allowance has
            no

            room for another contact), `sandbox_channel_not_included`
            (`permission`:

            the allowance does not cover this sandbox channel),
            `sign_in_required`

            (`permission`: an app made without an account cannot do this) or

            `sandbox_key_expired` (`authentication`: a key from `POST

            /v1/sandbox/keys` passed its `expires_at`; `permission`, for a send

            that carries no key, such as a reply in a webhook answer, from such
            an

            app). For `idempotency_conflict`, always one of `body_mismatch` (the

            key was used for a different request), `in_progress` (the first

            request is still running; `retry_after` says when to repeat it) or

            `secret_not_kept` (its answer carried a secret shown only once).
        request_id:
          type: string
          description: Flow's ID for this request. Quote it when asking for help.
    EventBase:
      type: object
      description: The fields every event has.
      required:
        - id
        - created_at
        - app
        - livemode
        - timing
      properties:
        id:
          $ref: '#/components/schemas/EventId'
        created_at:
          type: string
          format: date-time
          description: >-
            When the event was created. Events are ordered in the log by commit,
            not by this time.
        app:
          $ref: '#/components/schemas/AppId'
        livemode:
          type: boolean
          description: Whether the event belongs to live mode.
        conversation:
          $ref: '#/components/schemas/ConversationRef'
        timing:
          $ref: '#/components/schemas/Timing'
    MessageEventData:
      type: object
      description: The message the event is about.
      required:
        - message
      properties:
        message:
          $ref: '#/components/schemas/Message'
    ReactionEventData:
      type: object
      description: A reaction the contact added or removed.
      required:
        - message_id
        - emoji
      properties:
        message_id:
          $ref: '#/components/schemas/MessageId'
        emoji:
          type: string
          description: The emoji (iMessage tapbacks are mapped to their closest emoji).
    TypingEventData:
      type: object
      description: Who is typing.
      required:
        - contact
      properties:
        contact:
          $ref: '#/components/schemas/ContactId'
    ConversationStartedData:
      type: object
      description: The new conversation and how it began.
      required:
        - conversation
        - via
      properties:
        conversation:
          $ref: '#/components/schemas/Conversation'
        via:
          type: string
          description: >-
            `inbound` (the contact wrote first), `sandbox_join` (they sent your
            app's join code) or `outbound` (you started it).
          enum:
            - inbound
            - sandbox_join
            - outbound
    WindowClosingData:
      type: object
      description: When the window closes.
      required:
        - window_open_until
      properties:
        window_open_until:
          type: string
          format: date-time
          description: After this time only templates can be sent.
    SenderStatusChangedData:
      type: object
      description: The sender as it is now, and what changed.
      required:
        - sender
        - previous_status
        - reason
      properties:
        sender:
          $ref: '#/components/schemas/Sender'
        previous_status:
          $ref: '#/components/schemas/SenderStatus'
        reason:
          type: string
          description: >-
            Why, in one sentence (for example "Many new conversations got no
            reply; starts are slowed for 24 hours").
    TemplateStatusChangedData:
      type: object
      description: The template as it is now, and what changed.
      required:
        - template
        - previous_status
      properties:
        template:
          $ref: '#/components/schemas/Template'
        previous_status:
          $ref: '#/components/schemas/TemplateStatus'
        reason:
          type: string
          description: Meta's reason, when it gives one.
    ErrorType:
      type: string
      description: >
        What went wrong. A closed list: new types arrive only with a new API
        version.

        Each type has a page at `https://api.flow.engineer/docs/errors/<type>`,
        given in

        the error's `doc_url`; the error's `hint` says what to change for the
        case.


        - `invalid_request` (400): the request is malformed or a parameter is
        invalid. Fix the parameter named in `param`; the `hint` says what it
        must look like.

        - `authentication` (401): the API key is missing, malformed, unknown,
        revoked or expired. Send `Authorization: Bearer fk_test_...` or
        `fk_live_...` with a current key; with no key at all, get a test key
        with `POST /v1/sandbox/keys`. A sandbox key past its `expires_at` has
        `channel_code` `sandbox_key_expired`: sign in to claim the app, or get a
        new key.

        - `permission` (403): the key may not do this, for example a test key
        using a live sender, a sandbox contact who joined another app, a new
        conversation from an iMessage line that may only reply, or a send past
        the app's sandbox allowance (`channel_code` `sandbox_allowance_used`,
        `sandbox_contact_limit` or `sandbox_channel_not_included`). Use the key
        of the right mode, have the contact send your join code, wait for the
        contact to message the line first, or sign in through the device flow to
        lift the allowance.

        - `not_found` (404): no such object for this app and mode. Check the
        ID's prefix and that it was made with a key of the same mode (test and
        live data are separate).

        - `idempotency_conflict` (409): the idempotency key was used for a
        different request (`channel_code` `body_mismatch`: use a new key), or
        that request is still running (`in_progress`: wait `retry_after` seconds
        and repeat it with the same key), or it already created a secret that is
        shown only once (`secret_not_kept`: creating a webhook endpoint,
        rotating its secret; a secret that was lost must be rotated again).

        - `outside_window` (409, or in `message.failed`): the channel will not
        deliver outside its conversation window. On WhatsApp the 24-hour window
        is closed: send a `template` (`POST /v1/messages` with
        `content.type=template`), or wait for the contact to write. On iMessage
        the contact has not messaged the line (or opted in) yet, or not
        recently, so the failure arrives as a `message.failed` event: wait for
        the contact to write, then reply in that conversation. Typing and read
        receipts answer `409 outside_window` directly when the channel's window
        is closed (iMessage typing works only within 5 minutes of the contact's
        last message); ignore it and send your reply.

        - `unsupported_content` (422): the channel cannot show this content and
        no `fallback` was set. Set `fallback` (`"auto"` or your own content), or
        check `GET /v1/capabilities` first.

        - `new_contact_limit` (429): the sender has used its budget for starting
        conversations. Wait `retry_after` seconds; replies into existing
        conversations still go.

        - `sender_throttled` (429): abuse signals tripped (the same text to many
        new contacts, many starts with no reply, blocks), so the sender may not
        start conversations until `retry_after`; replies into existing
        conversations still go. Personalise first messages and start only
        conversations people expect.

        - `file_blocked` (422): the file failed the malware scan and was not
        stored. Send a different file.

        - `rate_limited` (429): either too many requests for this key (the
        per-key request limit, reported in the `RateLimit-*` headers) or sends
        faster than the sender's sending rate (pacing). Wait `Retry-After`
        (`retry_after`) seconds and retry with the same `Idempotency-Key`.

        - `channel_error` (502, or in `message.failed`): the channel refused or
        failed the message, or timed out; `channel_code` carries its own code.
        Read `message`, change what the channel objected to, and send again.
        From typing and read receipts, which call the channel at once, it is
        safe to ignore.

        - `not_implemented` (501): this endpoint or channel is not live yet
        during the beta. Use a channel that is live (Telegram, iMessage), or
        check the changelog.

        - `api_error` (500, 503): something went wrong on Flow's side. Retry
        with the same idempotency key after `retry_after` seconds, and quote
        `request_id` if it persists.
      enum:
        - invalid_request
        - authentication
        - permission
        - not_found
        - idempotency_conflict
        - outside_window
        - unsupported_content
        - new_contact_limit
        - sender_throttled
        - file_blocked
        - rate_limited
        - channel_error
        - not_implemented
        - api_error
    SenderId:
      type: string
      description: A sender ID, `snd_` and a ULID.
      pattern: ^snd_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T
    EventId:
      type: string
      description: An event ID, `evt_` and a ULID.
      pattern: ^evt_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - evt_01JB8ZE5N7Q9S1V3X5Z7B9D1E3
    AppId:
      type: string
      description: An app ID, `app_` and a ULID.
      pattern: ^app_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - app_01JB8Z0A1C3E5G7J9K1M3P5R7T
    ConversationRef:
      type: object
      description: >-
        The conversation an event happened in, inlined so most handlers need no
        extra call.
      required:
        - id
        - channel
        - sender
        - contact
      properties:
        id:
          $ref: '#/components/schemas/ConversationId'
        channel:
          $ref: '#/components/schemas/Channel'
        sender:
          $ref: '#/components/schemas/SenderId'
        contact:
          $ref: '#/components/schemas/ContactId'
        window_open_until:
          type: string
          format: date-time
          description: >-
            WhatsApp only. Until when free-form messages may be sent. Absent
            when closed or not applicable.
    Timing:
      type: object
      description: |
        When the event moved through Flow, to measure Flow's overhead
        (`delivered_at` minus `received_at`).
      required:
        - received_at
        - stored_at
      properties:
        received_at:
          type: string
          format: date-time
          description: >-
            When Flow received it from the channel, or when it happened inside
            Flow.
        stored_at:
          type: string
          format: date-time
          description: When it was committed to the log.
        delivered_at:
          type: string
          format: date-time
          description: When an endpoint first accepted it. Absent until then.
    Message:
      type: object
      description: >-
        One message in or out of a conversation, with its typed content and
        delivery status.
      required:
        - id
        - conversation
        - direction
        - status
        - content
        - livemode
        - created_at
      properties:
        id:
          $ref: '#/components/schemas/MessageId'
        conversation:
          $ref: '#/components/schemas/ConversationId'
        direction:
          type: string
          description: '`in` from the contact, `out` from your app.'
          enum:
            - in
            - out
        status:
          $ref: '#/components/schemas/MessageStatus'
        content:
          $ref: '#/components/schemas/Content'
        delivered_as:
          $ref: '#/components/schemas/DeliveredAs'
        error:
          $ref: '#/components/schemas/ErrorBody'
        channel_message_id:
          type: string
          description: The channel's own ID for the message, once the channel accepted it.
        reply_to:
          $ref: '#/components/schemas/MessageId'
          readOnly: true
          description: >
            The Flow ID of the message this one replies to. For a sent message,
            the

            message the send named in `reply_to`. For a received message, the
            message it

            replies to inline (a Telegram reply; on iMessage, the thread's root
            message).

            Absent when the message is not a reply or the quoted message is not
            known

            to Flow.
        livemode:
          type: boolean
          description: Whether the message belongs to live mode.
        metadata:
          $ref: '#/components/schemas/Metadata'
        created_at:
          type: string
          format: date-time
          description: When Flow received (inbound) or accepted (outbound) the message.
        updated_at:
          type: string
          format: date-time
          description: When the status last changed.
    MessageId:
      type: string
      description: A message ID, `msg_` and a ULID.
      pattern: ^msg_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2
    ContactId:
      type: string
      description: A contact ID, `ct_` and a ULID.
      pattern: ^ct_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - ct_01JB8ZB2J4K6N8Q0S2V4W6Y8A0
    Conversation:
      type: object
      description: >-
        One sender talking with one contact. Holds the window state and whether
        the contact opted in.
      required:
        - id
        - app
        - channel
        - sender
        - contact
        - livemode
        - opted_in
        - created_at
      properties:
        id:
          $ref: '#/components/schemas/ConversationId'
        app:
          $ref: '#/components/schemas/AppId'
        channel:
          $ref: '#/components/schemas/Channel'
        sender:
          $ref: '#/components/schemas/SenderId'
        contact:
          $ref: '#/components/schemas/ContactId'
        livemode:
          type: boolean
          description: Whether the conversation belongs to live mode.
        last_inbound_at:
          type: string
          format: date-time
          description: When the contact last wrote. Absent if they never have.
        window_open_until:
          type: string
          format: date-time
          description: >-
            WhatsApp only. Until when free-form messages may be sent; after it,
            only templates. Absent when the window is closed or the channel has
            none.
        opted_in:
          type: boolean
          description: >-
            Whether the contact has agreed to receive messages (wrote first,
            joined the sandbox, or you recorded consent).
        metadata:
          $ref: '#/components/schemas/Metadata'
        created_at:
          type: string
          format: date-time
          description: When the conversation began.
    Sender:
      type: object
      description: >
        What your agent talks from: a Telegram bot, a WhatsApp number or an
        iMessage

        line. `shared` senders are Flow's sandbox, used by many apps in test
        mode;

        `dedicated` senders are yours alone. Each sender has its own limits and

        warm-up state.
      required:
        - id
        - channel
        - kind
        - livemode
        - status
        - address
        - limits
        - created_at
      properties:
        id:
          $ref: '#/components/schemas/SenderId'
        channel:
          $ref: '#/components/schemas/Channel'
        kind:
          type: string
          description: '`shared` (the sandbox) or `dedicated` (yours).'
          enum:
            - shared
            - dedicated
        livemode:
          type: boolean
          description: >-
            `false` for sandbox senders, `true` for dedicated senders used in
            live mode.
        display_name:
          type: string
          description: The name contacts see, where the channel shows one.
        status:
          $ref: '#/components/schemas/SenderStatus'
        throttled_until:
          type: string
          format: date-time
          description: >-
            With status `throttled`, when starts are allowed again. Recovery is
            automatic.
        address:
          $ref: '#/components/schemas/SenderAddress'
        limits:
          $ref: '#/components/schemas/SenderLimits'
        quality_rating:
          type: string
          description: WhatsApp only. Meta's quality rating for the number.
          enum:
            - green
            - yellow
            - red
            - unknown
        join_code:
          type: string
          description: >
            Shared senders only. The whole message a contact sends to this
            sender to join your app: `join `, a space, then the app's
            `sandbox_join_code` (from `GET /v1/app`), for example `join
            brave-otter-40718263`. Show it to testers as is.
          examples:
            - join brave-otter-40718263
        created_at:
          type: string
          format: date-time
          description: When the sender was created.
    SenderStatus:
      type: string
      description: >
        - `pending`: requested, being provisioned.

        - `active`: sending normally.

        - `warming_up`: active, with a new-contact budget that grows day by day.

        - `throttled`: the gate slowed it after an abuse signal; it recovers by
        itself.

        - `flagged`: the channel or Flow flagged it; starts are paused. A
        Telegram
          bot is also `flagged` when Telegram rejects its token (revoked in
          @BotFather): it then sends nothing, new sends answer `403 permission`, and
          queued messages wait until you connect the bot again with its new token
          (`POST /v1/senders`). They wait at most 72 hours after Flow accepted
          them; older ones fail with `outside_window` (`channel_code`
          `queued_too_long`) in `message.failed` instead of going out late.
        - `banned`: it cannot send or receive: the channel banned it, you
          disconnected it (`DELETE /v1/senders/{sender_id}`), or its bot or line was
          connected to another app.
      enum:
        - pending
        - active
        - warming_up
        - throttled
        - flagged
        - banned
    Template:
      type: object
      description: >-
        A WhatsApp message template, mirrored from Meta with its approval
        status.
      required:
        - id
        - sender
        - name
        - language
        - category
        - status
        - components
        - created_at
      properties:
        id:
          $ref: '#/components/schemas/TemplateId'
        sender:
          $ref: '#/components/schemas/SenderId'
        name:
          type: string
          description: The template's name at Meta.
        language:
          type: string
          description: The template's language, as Meta names it.
        category:
          $ref: '#/components/schemas/TemplateCategory'
        status:
          $ref: '#/components/schemas/TemplateStatus'
        components:
          type: array
          description: The template's parts, as submitted.
          items:
            $ref: '#/components/schemas/TemplateComponent'
        rejection_reason:
          type: string
          description: Meta's reason, when the template was rejected.
        created_at:
          type: string
          format: date-time
          description: When the template was submitted.
    TemplateStatus:
      type: string
      description: A template's approval status at Meta.
      enum:
        - pending
        - approved
        - rejected
        - paused
        - disabled
    Channel:
      type: string
      description: A messaging channel.
      enum:
        - telegram
        - whatsapp
        - imessage
    MessageStatus:
      type: string
      description: >
        - `received`: inbound from the contact.

        - `queued`: accepted by the gate, waiting for its turn in the
        conversation.

        - `sent`: the channel accepted it.

        - `delivered`: it reached the contact's device, where the channel
        reports this.

        - `read`: the contact read it, where the channel reports this.

        - `failed`: it could not be sent; see `error`.

        - `unsent`: you unsent it.
      enum:
        - received
        - queued
        - sent
        - delivered
        - read
        - failed
        - unsent
    Content:
      type: object
      description: >
        One piece of content, in either direction. `type` selects the shape.
        Inbound

        messages use `text`, `media`, `voice`, `button_reply`, `reaction`,
        `location`,

        `contact_card` and `file_blocked`; sends use every type except
        `button_reply`

        and `file_blocked`.


        | type | Telegram | WhatsApp | iMessage | `fallback: "auto"` |

        |---|---|---|---|---|

        | text | yes | yes | yes | markdown to plain |

        | media | yes | yes | yes | none |

        | voice | yes | yes | yes | audio file |

        | buttons | inline keyboard | up to 3 buttons, else a list | no |
        numbered text |

        | reaction | yes | yes | tapbacks, other emoji as emoji reactions |
        closest tapback, else skipped |

        | template | no | yes | no | none |

        | location | yes | yes | no | maps link as text |

        | contact_card | yes | yes | no | text |

        | effect | no | no | yes | plain text |

        | typing | yes | yes | yes (within 5 minutes of the contact's last
        message) | skipped |

        | read | no (bots) | yes | yes (the whole conversation) | skipped |

        | edit | yes | no | yes (within 15 minutes) | none |

        | unsend | yes (within 48 hours) | no | yes (within 2 minutes) | none |
      oneOf:
        - $ref: '#/components/schemas/TextContent'
        - $ref: '#/components/schemas/MediaContent'
        - $ref: '#/components/schemas/VoiceContent'
        - $ref: '#/components/schemas/ButtonsContent'
        - $ref: '#/components/schemas/ButtonReplyContent'
        - $ref: '#/components/schemas/ReactionContent'
        - $ref: '#/components/schemas/TemplateContent'
        - $ref: '#/components/schemas/LocationContent'
        - $ref: '#/components/schemas/ContactCardContent'
        - $ref: '#/components/schemas/EffectContent'
        - $ref: '#/components/schemas/TypingContent'
        - $ref: '#/components/schemas/ReadContent'
        - $ref: '#/components/schemas/EditContent'
        - $ref: '#/components/schemas/UnsendContent'
        - $ref: '#/components/schemas/FileBlockedContent'
      discriminator:
        propertyName: type
        mapping:
          text: '#/components/schemas/TextContent'
          media: '#/components/schemas/MediaContent'
          voice: '#/components/schemas/VoiceContent'
          buttons: '#/components/schemas/ButtonsContent'
          button_reply: '#/components/schemas/ButtonReplyContent'
          reaction: '#/components/schemas/ReactionContent'
          template: '#/components/schemas/TemplateContent'
          location: '#/components/schemas/LocationContent'
          contact_card: '#/components/schemas/ContactCardContent'
          effect: '#/components/schemas/EffectContent'
          typing: '#/components/schemas/TypingContent'
          read: '#/components/schemas/ReadContent'
          edit: '#/components/schemas/EditContent'
          unsend: '#/components/schemas/UnsendContent'
          file_blocked: '#/components/schemas/FileBlockedContent'
    DeliveredAs:
      type: object
      description: >
        Present when what the contact sees differs from what you sent: the
        `fallback`

        was used, or the action was skipped because the channel has no
        equivalent.

        Absent when the content was shown as sent.
      required:
        - type
        - reason
      properties:
        type:
          type: string
          description: The content type actually shown, or `none` when nothing was shown.
          enum:
            - text
            - media
            - voice
            - buttons
            - reaction
            - template
            - location
            - contact_card
            - effect
            - typing
            - read
            - edit
            - unsend
            - none
        reason:
          type: string
          description: >-
            Why, in one sentence (for example "iMessage cannot show buttons;
            sent numbered text").
    Metadata:
      type: object
      description: >-
        Up to 20 string pairs of your own, kept with the object and returned
        unchanged. Keys up to 40 characters, values up to 500.
      maxProperties: 20
      additionalProperties:
        type: string
        maxLength: 500
    SenderAddress:
      type: object
      description: >-
        How contacts reach the sender. Which fields are set depends on the
        channel.
      properties:
        phone:
          type: string
          description: WhatsApp and iMessage. E.164 phone number.
        username:
          type: string
          description: Telegram. The bot's username, without `@`.
        handle:
          type: string
          description: iMessage. The line's handle (phone number or email address).
        link:
          type: string
          format: uri
          description: >
            A link that opens a chat with the sender: `https://t.me/...` for a
            Telegram

            bot, `https://wa.me/...` for a WhatsApp number. For an iMessage line
            it is

            the line's opt-in link, which opens Messages with the line and a
            prefilled

            text the person sends to start the conversation; it is set only when
            the

            line has one configured.
    SenderLimits:
      type: object
      description: The sender's current budget for starting conversations.
      required:
        - new_contacts_per_day
        - new_contacts_per_hour
      properties:
        new_contacts_per_day:
          type: integer
          description: >-
            New conversations the sender may start per day today (grows during
            warm-up).
        new_contacts_per_hour:
          type: integer
          description: New conversations the sender may start per hour.
        whatsapp_tier:
          type: string
          description: WhatsApp only. Meta's messaging tier for the number.
    TemplateId:
      type: string
      description: A template ID, `tpl_` and a ULID.
      pattern: ^tpl_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V
    TemplateCategory:
      type: string
      description: Meta's category for the template, which sets its price.
      enum:
        - marketing
        - utility
        - authentication
    TemplateComponent:
      type: object
      description: One part of a template. Placeholders are written `{{1}}`, `{{2}}`, ...
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - header
            - body
            - footer
            - buttons
          description: Which part this is.
        format:
          type: string
          enum:
            - text
            - image
            - video
            - document
          description: For headers, what the header holds.
        text:
          type: string
          description: The part's text, with placeholders.
        buttons:
          type: array
          description: For `buttons`, the buttons in order.
          items:
            $ref: '#/components/schemas/TemplateButton'
    TextContent:
      type: object
      description: >-
        Text, both ways. With `format` `markdown`, Flow renders it in each
        channel's own formatting; a channel without formatting needs `fallback`.
      required:
        - type
        - text
      properties:
        type:
          type: string
          enum:
            - text
          description: Always `text`.
        text:
          type: string
          minLength: 1
          maxLength: 9999
          description: >-
            The text, at most the channel's `max_text_length` characters (4096
            on Telegram and WhatsApp, 9999 on iMessage; see `GET
            /v1/capabilities`). Telegram counts UTF-16 code units of the text as
            shown (after markdown), so an emoji such as 😀 counts as 2. In an
            edit of a media message the text replaces its caption, and the
            caption limit applies (1024). Longer text is refused with
            `invalid_request`.
        format:
          type: string
          enum:
            - plain
            - markdown
          default: plain
          description: How to read `text`. Inbound text is always `plain`.
    MediaContent:
      type: object
      description: >
        An image, video, document or audio file, both ways. When sending, give
        either

        `url` (Flow fetches it) or `file_id` (from `POST /v1/files`). Inbound
        media

        always has `url`, a Flow file URL (`GET /v1/files/{file_id}`), and
        `file_id`.
      required:
        - type
        - kind
      properties:
        type:
          type: string
          enum:
            - media
          description: Always `media`.
        kind:
          type: string
          enum:
            - image
            - video
            - document
            - audio
          description: What kind of media it is.
        url:
          type: string
          format: uri
          description: >-
            Where the bytes are. Sending, an HTTPS URL Flow can fetch; inbound,
            a Flow file URL.
        file_id:
          $ref: '#/components/schemas/FileId'
        caption:
          type: string
          maxLength: 1024
          description: >-
            Text shown with the media, at most 1024 characters. Telegram counts
            UTF-16 code units, so an emoji such as 😀 counts as 2. Longer
            captions are refused with `invalid_request`.
        filename:
          type: string
          maxLength: 255
          description: The file's name, shown for documents.
        mime_type:
          type: string
          readOnly: true
          description: The real type, checked from the file's first bytes.
        size_bytes:
          type: integer
          readOnly: true
          description: The file's size.
    VoiceContent:
      type: object
      description: >
        A voice note, both ways. Inbound voice notes always come with the audio
        file

        (`url`, `file_id`) and, when the app has transcription on, a
        `transcript`.

        When sending, give `url` or `file_id` of an audio file; channels without

        voice notes need `fallback` (`auto` sends it as an audio file).
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - voice
          description: Always `voice`.
        url:
          type: string
          format: uri
          description: >-
            Where the audio is. Sending, an HTTPS URL Flow can fetch; inbound, a
            Flow file URL.
        file_id:
          $ref: '#/components/schemas/FileId'
        duration_seconds:
          type: number
          readOnly: true
          description: The voice note's length.
        transcript:
          type: string
          readOnly: true
          description: Inbound only, when transcription is on. What was said.
    ButtonsContent:
      type: object
      description: >
        Text with buttons, sent only. Telegram shows an inline keyboard;
        WhatsApp shows

        up to 3 reply buttons, or a list for more; iMessage has no buttons (use

        `fallback`). A tap arrives as a `message.received` event with
        `button_reply`

        content carrying the button's `id`. URL buttons open a link and send
        nothing

        back.
      required:
        - type
        - text
        - buttons
      properties:
        type:
          type: string
          enum:
            - buttons
          description: Always `buttons`.
        text:
          type: string
          minLength: 1
          maxLength: 4096
          description: >
            The text above the buttons: at most 4096 characters on Telegram (the
            same

            as a text message, in UTF-16 code units; Telegram's 1024 limit is
            for media

            captions only) and 1024 on WhatsApp (its limit for messages with
            buttons). On iMessage,

            `fallback: auto` sends the text and the numbered choices as one text

            message, which must fit the channel's `max_text_length`. Longer text
            is

            refused with `invalid_request`.
        buttons:
          type: array
          minItems: 1
          maxItems: 10
          description: 1 to 10 buttons, in order.
          items:
            $ref: '#/components/schemas/Button'
    ButtonReplyContent:
      type: object
      description: >-
        Inbound only. The contact tapped a reply button (or, where buttons fell
        back to numbered text, answered with its number).
      required:
        - type
        - button_id
        - label
      properties:
        type:
          type: string
          enum:
            - button_reply
          description: Always `button_reply`.
        button_id:
          type: string
          description: The `id` of the tapped button.
        label:
          type: string
          description: The tapped button's label.
        message_id:
          $ref: '#/components/schemas/MessageId'
    ReactionContent:
      type: object
      description: >
        A reaction to a message, both ways. Set `emoji` to `null` to remove your

        reaction. iMessage has a fixed set of tapbacks; with `fallback: "auto"`
        the

        closest tapback is used, or the reaction is skipped and reported in

        `delivered_as`. Inbound reactions also arrive as `reaction.added` and

        `reaction.removed` events.
      required:
        - type
        - message_id
        - emoji
      properties:
        type:
          type: string
          enum:
            - reaction
          description: Always `reaction`.
        message_id:
          $ref: '#/components/schemas/MessageId'
        emoji:
          type:
            - string
            - 'null'
          maxLength: 16
          description: One emoji, or `null` to remove the reaction.
    TemplateContent:
      type: object
      description: >
        A WhatsApp message template, sent only. Required to start a WhatsApp

        conversation or to write after the 24-hour window closed. The template
        must be

        `approved`.
      required:
        - type
        - template_id
        - language
      properties:
        type:
          type: string
          enum:
            - template
          description: Always `template`.
        template_id:
          $ref: '#/components/schemas/TemplateId'
        language:
          type: string
          description: >-
            The template language to use, as Meta names it (`en`, `en_US`,
            `hi`).
        params:
          $ref: '#/components/schemas/TemplateParams'
    LocationContent:
      type: object
      description: >-
        A place, both ways. Inbound, a location the contact shared. Channels
        without locations need `fallback` (`auto` sends a maps link).
      required:
        - type
        - lat
        - lng
      properties:
        type:
          type: string
          enum:
            - location
          description: Always `location`.
        lat:
          type: number
          minimum: -90
          maximum: 90
          description: Latitude in degrees.
        lng:
          type: number
          minimum: -180
          maximum: 180
          description: Longitude in degrees.
        name:
          type: string
          description: The place's name.
        address:
          type: string
          description: The place's address.
    ContactCardContent:
      type: object
      description: >-
        A contact card, both ways. Channels without cards need `fallback`
        (`auto` sends it as text).
      required:
        - type
        - name
      properties:
        type:
          type: string
          enum:
            - contact_card
          description: Always `contact_card`.
        name:
          type: string
          description: The person's or business's name.
        phones:
          type: array
          description: Phone numbers, E.164 where known.
          items:
            type: string
        emails:
          type: array
          description: Email addresses.
          items:
            type: string
            format: email
    EffectContent:
      type: object
      description: >-
        Text sent with an iMessage effect, sent only. Other channels need
        `fallback` (`auto` sends the plain text). Which effects are available
        may vary.
      required:
        - type
        - text
        - effect
      properties:
        type:
          type: string
          enum:
            - effect
          description: Always `effect`.
        text:
          type: string
          minLength: 1
          maxLength: 9999
          description: >-
            The text, at most the channel's `max_text_length` characters (4096
            on Telegram and WhatsApp, 9999 on iMessage; see `GET
            /v1/capabilities`). Telegram counts UTF-16 code units, so an emoji
            such as 😀 counts as 2. Longer text is refused with
            `invalid_request`.
        effect:
          type: string
          description: The effect.
          enum:
            - slam
            - loud
            - gentle
            - invisible_ink
            - echo
            - spotlight
            - balloons
            - confetti
            - love
            - lasers
            - fireworks
            - celebration
    TypingContent:
      type: object
      description: >-
        The typing indicator, sent only (the stream's and webhook replies' way
        to type; over HTTP use `POST
        /v1/conversations/{conversation_id}/typing`). Skipped where the channel
        has none.
      required:
        - type
        - state
      properties:
        type:
          type: string
          enum:
            - typing
          description: Always `typing`.
        state:
          type: string
          description: '`on` shows the indicator, `off` clears it.'
          enum:
            - 'on'
            - 'off'
    ReadContent:
      type: object
      description: >-
        A read receipt, sent only (over HTTP use `POST
        /v1/conversations/{conversation_id}/read`). Skipped on Telegram, where
        bots cannot send them. On iMessage the whole conversation is marked
        read.
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - read
          description: Always `read`.
        up_to:
          $ref: '#/components/schemas/MessageId'
          description: >-
            The latest inbound message to mark read (default the latest inbound
            message). No effect on iMessage.
    EditContent:
      type: object
      description: >-
        Replaces the text of one of your sent messages, sent only (over HTTP use
        `PATCH /v1/messages/{message_id}`). Telegram and iMessage; WhatsApp
        refuses it with `unsupported_content`.
      required:
        - type
        - message_id
        - content
      properties:
        type:
          type: string
          enum:
            - edit
          description: Always `edit`.
        message_id:
          $ref: '#/components/schemas/MessageId'
        content:
          $ref: '#/components/schemas/TextContent'
    UnsendContent:
      type: object
      description: >-
        Removes one of your sent messages from the contact's chat, sent only
        (over HTTP use `DELETE /v1/messages/{message_id}`). Telegram and
        iMessage; WhatsApp refuses it with `unsupported_content`.
      required:
        - type
        - message_id
      properties:
        type:
          type: string
          enum:
            - unsend
          description: Always `unsend`.
        message_id:
          $ref: '#/components/schemas/MessageId'
    FileBlockedContent:
      type: object
      description: >
        Inbound only. The contact sent a file that Flow did not keep: it failed
        the

        malware scan, its real type is one Flow does not pass on (programs, web

        pages), it could not be scanned, or it is larger than the channel lets
        Flow

        download. The file is not stored and has no `url`; what is known about
        it is

        here. Images, audio and video are not scanned; documents (PDF, Office,

        archives) are.
      required:
        - type
        - reason
      properties:
        type:
          type: string
          enum:
            - file_blocked
          description: Always `file_blocked`.
        reason:
          type: string
          enum:
            - malware
            - type_not_allowed
            - scan_failed
            - too_large
          description: >-
            Why the file was not kept: `malware` (the scan found something),
            `type_not_allowed`, `scan_failed` (the scanner could not check it;
            ask the contact to send it again), or `too_large`.
        kind:
          type: string
          enum:
            - image
            - video
            - document
            - audio
          description: What kind of media the channel said it was.
        filename:
          type: string
          description: The file's name, if it had one.
        mime_type:
          type: string
          description: >-
            The real type, read from the file's first bytes, when Flow read
            them.
        size_bytes:
          type: integer
          description: The file's size, when known.
        caption:
          type: string
          description: Text the contact sent with the file.
    TemplateButton:
      type: object
      description: One button of a template.
      required:
        - type
        - text
      properties:
        type:
          type: string
          enum:
            - quick_reply
            - url
            - phone_number
          description: What the button does.
        text:
          type: string
          description: The button's label.
        url:
          type: string
          description: >-
            For `url` buttons, the link, optionally ending in a `{{1}}`
            placeholder.
        phone:
          type: string
          description: For `phone_number` buttons, the E.164 number.
    FileId:
      type: string
      description: A file ID, `file_` and a ULID.
      pattern: ^file_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - file_01JB8ZG7Q9S1V3X5Z7B9D1F3G5
    Button:
      description: A reply button (`id` and `label`) or a link button (`url` and `label`).
      oneOf:
        - $ref: '#/components/schemas/ReplyButton'
        - $ref: '#/components/schemas/UrlButton'
    TemplateParams:
      type: object
      description: Values for the template's placeholders.
      properties:
        header:
          $ref: '#/components/schemas/TemplateHeaderParam'
        body:
          type: array
          description: Values for the body's `{{1}}`, `{{2}}`, ... in order.
          items:
            type: string
        buttons:
          type: array
          description: >-
            Values for buttons that take one (URL suffixes, quick-reply
            payloads).
          items:
            $ref: '#/components/schemas/TemplateButtonParam'
    ReplyButton:
      type: object
      description: A button that sends its `id` back as a `button_reply` when tapped.
      required:
        - id
        - label
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
          description: Your ID for the button, returned in `button_reply`.
        label:
          type: string
          minLength: 1
          maxLength: 20
          description: The button's text. WhatsApp shows at most 20 characters.
    UrlButton:
      type: object
      description: A button that opens a link.
      required:
        - url
        - label
      properties:
        url:
          type: string
          format: uri
          description: The HTTPS link to open.
        label:
          type: string
          minLength: 1
          maxLength: 20
          description: The button's text.
    TemplateHeaderParam:
      type: object
      description: The value of a template's header, when it has a placeholder or media.
      required:
        - kind
      properties:
        kind:
          type: string
          enum:
            - text
            - image
            - video
            - document
          description: What the header holds.
        text:
          type: string
          description: For `text` headers, the placeholder's value.
        url:
          type: string
          format: uri
          description: For media headers, an HTTPS URL of the media.
        file_id:
          $ref: '#/components/schemas/FileId'
    TemplateButtonParam:
      type: object
      description: The value for one of the template's buttons.
      required:
        - index
        - value
      properties:
        index:
          type: integer
          minimum: 0
          maximum: 9
          description: The button's position in the template, from 0.
        value:
          type: string
          description: The URL suffix or the quick-reply payload.
  responses:
    InvalidRequest:
      description: >-
        The request is malformed or a parameter is invalid (`invalid_request`).
        `error.param` names the parameter.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: invalid_request
              message: limit must be between 1 and 100.
              hint: >-
                Pass limit between 1 and 100 (default 20), and page with after
                or before.
              doc_url: https://api.flow.engineer/docs/errors/invalid_request
              param: limit
    Unauthenticated:
      description: >-
        The API key is missing, malformed, unknown, revoked or expired
        (`authentication`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: authentication
              message: No valid API key was given.
              hint: >-
                Send the header Authorization: Bearer fk_test_... (or
                fk_live_...); no key yet? Get a test key with curl -X POST
                https://api.flow.engineer/v1/sandbox/keys
              doc_url: https://api.flow.engineer/docs/errors/authentication
    NotFound:
      description: No such object for this app and mode (`not_found`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: >
        Too many requests (`rate_limited`): either this key's request limit (see
        the

        `RateLimit-*` headers) or the sender's sending rate (pacing) was
        exceeded;

        retry after `Retry-After` seconds (also `error.retry_after`) with the
        same

        `Idempotency-Key`. Or the sender has used its budget for starting

        conversations (`new_contact_limit`), or the sender is throttled after
        abuse

        signals (`sender_throttled`); wait `retry_after` seconds.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              type: new_contact_limit
              message: This sender has started its 15 new conversations for today.
              hint: >-
                Retry after 3600 seconds; replies into existing conversations
                still go.
              doc_url: https://api.flow.engineer/docs/errors/new_contact_limit
              retry_after: 3600
              sender: snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T
    Error:
      description: An error. `error.type` says which (see `ErrorType`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  headers:
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
    RateLimitLimit:
      description: Requests allowed in the current window for this key.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests left in the current window for this key.
      schema:
        type: integer
    RateLimitReset:
      description: Seconds until the current window resets.
      schema:
        type: integer
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: fk_test_... or fk_live_...
      description: >
        An API key of one app, sent as `Authorization: Bearer <key>`. Keys start
        with

        `fk_test_` (test mode: sandbox senders and test data only) or `fk_live_`

        (live mode). Keep live keys on your server; never ship them in an app or
        page.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.