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

# Open the live event stream (WebSocket)

> Upgrades to a WebSocket that pushes your app's events as they happen, in log order. Pass `after` to resume from the last event you saw: the stream first replays everything after it, then goes live, so a reconnect never loses an event. Every WebSocket text frame is one JSON object (`StreamFrame`). The server sends `event` frames; the client may send `send` frames with the same bodies as the HTTP send endpoints, and gets one `ack` or `error` frame back for each, matched by `ref`. When the server is about to restart it sends a `reconnect` frame; reconnect with `after` set to the last event you received. Authenticate with the `Authorization` header where your WebSocket client can set headers. Where it cannot (a browser's `WebSocket`, and Node's global `WebSocket` on the server), offer the key as a WebSocket subprotocol instead: offer both `flow` and `flow.key.<api key>`, for example `new WebSocket("wss://api.flow.engineer/v0/stream", ["flow", "flow.key." + key])`. This works from server-side clients as well as browsers. The server selects `flow` and never echoes the key. Offering the key protocol without `flow` is refused with a plain `400 invalid_request` answer, without an upgrade. When an `Authorization` header is present it takes precedence. Keys are never accepted in the query string, since URLs end up in logs. A key used in a browser is visible to whoever uses that page: do this only for internal tools or with test keys (`fk_test_`). **Refusals arrive on the socket.** Many WebSocket clients (Node's and browsers' among them) cannot read the HTTP status of a refused upgrade, so a WebSocket request that Flow refuses (a missing, unknown, revoked or expired key, a bad parameter, too many streams for the key, a restart) is still upgraded: the server sends one `error` frame with the usual error body (`type`, `message`, `hint`, `retry_after`, `channel_code`, ...), then closes with an application close code of 4000 plus the HTTP status the error has elsewhere, and a short reason: - `4401`: `authentication`. The key is missing, malformed, unknown, revoked or expired (`channel_code` `sandbox_key_expired`). Stop reconnecting until you have a working key. - `4403`: `permission`. The key may not open this stream. Stop reconnecting. - `4400`: `invalid_request`, for example a bad `after` or `type`. Fix the request; reconnecting unchanged fails again. - `4429`: `rate_limited`, for example more open streams than the key may hold. Reconnect after `retry_after` seconds. - `4500`, `4503`: Flow could not open the stream just now, or is restarting. Reconnect after `retry_after` seconds with the same `after`. An open stream re-checks its key about once a minute: when the key is revoked, the stream sends an `authentication` error frame and closes with `4401` too. Other closes: `1012` after a `reconnect` frame (reconnect at once with `after`), `1008` after too many rate-limited `send` frames in a row, `1001` when pings go unanswered, `1011` when the event log is unavailable; reconnect with `after` after any of these. A request without a WebSocket upgrade gets the same errors as plain HTTP answers.



## OpenAPI

````yaml unified-messaging-api/openapi.yaml GET /v0/stream
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, and on **SMS and MMS in the
    US** (in pilot: dedicated numbers arranged with the Flow team). 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 on lines the Flow team
    connects to your app: it replies to people who write first, in one-to-one
    conversations and in group chats people add the line to; writing first is
    available on request on dedicated lines (`Sender.imessage.can_initiate`). WhatsApp numbers are connected by the Flow team
    too, either a number Flow provides or one in your business's own WhatsApp
    Business Account, and the WhatsApp-only parts (templates, the 24-hour window)
    apply on them; the shared WhatsApp sandbox is not open yet.

    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,
      an iMessage line or a US SMS number. 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 (`type` `direct`),
      or, on iMessage, with a group chat the line was added to (`type` `group`,
      see Groups). You reply into a conversation; you never pick a channel per
      message.
    - A **message** is one **kind**: `basic` (text, or media with a caption),
      `voice`, `buttons`, `list`, `carousel`, `template`, `contact_card`,
      `location`, `effect`, `reaction`, and more (see `MessageKind`). Each kind
      you can send has its own endpoint, named after it
      (`POST /v0/conversations/{conversation_id}/basic`, `/buttons`, ...), whose
      page opens with the channels it works on. The same kind objects describe
      what you receive (`Message.content`, with `kind`).
    - 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 /v0/stream`) and by polling
      (`GET /v0/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 /v0/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/v0/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 message 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 a Flow account (flow.engineer)
    through the device flow: the agent or the CLI (`npx @flow-engineer/messaging login`) calls `POST
    /v0/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 /v0/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 /v0/app` shows what is left of the allowance
    (`allowance`). When it runs out, sends answer `403 permission` with
    `channel_code` `sandbox_allowance_used`. The apps of your own account get the
    free trial instead, started at your first sign-in: 25 people per channel on
    Flow's shared Telegram, WhatsApp and iMessage senders, over all the
    account's apps (their `allowance` is absent).

    ## 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`.

    Every path starts with `/v0`: this is API V0, the shape before the stable
    release. `/v1` is kept for that release, version 1.0, after which the shape is
    frozen.

    ## Idempotency

    Every `POST` that takes an API key accepts an `Idempotency-Key` header (any
    unique string up to 255 characters; a UUID or ULID is a good choice); the
    onboarding calls `POST /v0/sandbox/keys`, `POST /v0/device/authorizations` and
    `POST /v0/device/token` ignore it. 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,
      connecting a WhatsApp number), 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": "unsupported_content",
      "channel": "telegram",
      "param": "kind",
      "message": "Carousels work on WhatsApp only.",
      "hint": "On Telegram, send a basic message, then one buttons message with an image header per card.",
      "doc_url": "https://api.flow.engineer/docs/errors/unsupported_content",
      "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 /v0/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, which also refuses what the channel does not take (see "Exact or 422"):

    - **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
      (`Sender.imessage.can_initiate`, available on request on dedicated lines); 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, with a
      warm-up curve, and a contact who has never messaged the line (or opted in)
      gets a `message.failed` event with `outside_window`. Group conversations
      always begin with the group writing; a start never makes one.
    - **Telegram**: the contact must have started the bot; Telegram enforces this.
    - **SMS (US, in pilot)**: a conversation starts only with a contact who texted
      one of your numbers before, or whose consent you record with the start
      (`consent` on `POST /v0/messages`); otherwise `403 permission` with
      `channel_code` `consent_required`. A contact who texts STOP (or STOPALL,
      UNSUBSCRIBE, CANCEL, END, QUIT, REVOKE, OPT OUT, OPTOUT) is opted out of every
      SMS sender of your app at once: sends to them answer `403 permission` with
      `channel_code` `contact_opted_out`, and messages already queued fail the same
      way, until they text START (or UNSTOP, YES) to a number they opted out
      through (carriers and providers block per number, so START to another of
      your numbers changes nothing). HELP (or INFO) gets the sender's
      help reply. Flow answers these keywords itself, and you still receive the
      message as `message.received`. The contact's `opted_out_at` shows the state.
      Messages to a contact who has not texted in the last hour respect the sender's
      quiet hours (`Sender.sms.quiet_hours`, by default 21:00 to 08:00 in the
      contact's time zone, worked out from the area code): inside them a send
      answers `409 outside_window` with `channel_code` `quiet_hours` and
      `retry_after` set to when they end; a message that waited in the queue into
      the quiet hours is held until they end, not sent at night.
    - **HIPAA accounts** (`Account.hipaa`): only channels under a business
      associate agreement carry messages (SMS). Sends, typing and read receipts on
      Telegram, WhatsApp and iMessage, and connecting senders there, answer `403
      permission` with `channel_code` `hipaa_channel_not_covered`; messages
      already queued there fail the same way. Contacts get a fixed refusal from
      the sandbox senders instead of reaching the app, and nothing they send
      there is stored or delivered. Simulated conversations (`POST
      /v0/test/conversations`) follow the same rule: only SMS can be simulated.
    - **Sandbox senders**: only contacts who joined through your app (by sending its
      join message, for example `Hi Sandbox app (ref brave-otter-40718263)`, or by
      opening its Telegram link).

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

    ## Groups (iMessage)

    When someone adds one of your dedicated iMessage lines to a group chat (Flow's
    shared iMessage line ignores group chats), the group is one
    conversation with `type` `group`. It has no `contact`; `group` lists the
    participants (`participants`, each a Flow contact on the channel, made the first
    time Flow sees them) and the group's name when it has one. Every inbound
    message carries `from`, the contact who wrote it. Participants who join or
    leave change the list without an event: read the conversation
    (`GET /v0/conversations/{conversation_id}`) for the current list.

    Reply with the normal sends (`POST /v0/conversations/{conversation_id}/basic`
    and the other kinds' endpoints). Basic messages (with `imessage.styles`,
    `imessage.subject` and `link_preview`), voice notes, effects, `reply_to`, edits
    and unsends work in groups. Groups take no reactions, typing indicator or read
    receipts from your line: each answers `422 unsupported_content` with `param`
    `kind` and `channel_code` `group_unsupported`, and nothing is sent. Dedicated
    iMessage lines receive and reply in groups; Flow's shared iMessage line ignores
    group chats. A group cannot be started, renamed, or have people added or
    removed through the API.

    ## Exact or 422

    What you send is what the contact gets. Each endpoint's page opens with the
    channels it works on and their limits; a send of a kind, a field, a channel
    block or a value the conversation's channel does not take fails with `422
    unsupported_content` and sends nothing. `param` names what was refused
    (`kind`, a field such as `reply_to` or `telegram.is_big`, or a value's field
    such as `emoji`), `channel` the conversation's channel, and `hint` what to send
    instead over plain HTTP (for example, for a carousel on Telegram, a basic
    message, then one buttons message with an image header per card). Flow never
    converts a message into another kind, never drops a field, and never answers
    `2xx` for something it did not send. Fields that only one channel has sit in
    a block named after it (`telegram`, `imessage`), typed per endpoint; an
    unknown key in a block, or an unknown field, is refused with `400
    invalid_request` naming it, and so is a read-only field Flow fills on
    received messages (such as `media.mime_type` or a voice note's `transcript`).

    `text` is sent exactly as written on every channel: Flow does not read
    markdown, strip marks or warn. Style it in the channel's own terms:
    `telegram.parse_mode`, `imessage.styles`; WhatsApp's app reads its own
    `*bold*` and `_italic_`. With `telegram.parse_mode`, Telegram's length limits
    count the text after its tags or marks are parsed, as Telegram counts them, so
    a styled text may be longer than the limit before parsing.

    ## Order and duplicates

    Sends to one conversation go out in the order Flow accepted them, one at a
    time, across every endpoint, webhook replies and the stream. A message sent
    as several (a text and one message per card) arrives in the order you sent
    the calls. Give each call its own `Idempotency-Key` (for example
    `evt_123:card:2` for the second card answering event `evt_123`): a retry with
    the same key never sends twice (see Idempotency). A `422` answer is kept
    under its key like any other, so a message you send instead of a refused one
    (a `basic` after a `buttons` came back `422`) is a new request with a new
    key: add what it is to the refused one's key, such as `evt_123:card:2:basic`.

    ## Webhooks

    Register endpoints with `POST /v0/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
    /v0/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": <message>}` or `{"reply": [<message>, ...]}`
    (up to 11 pieces, each a kind object with `kind`, such as `{"kind": "buttons",
    ...}`); the reply is sent into the event's conversation through the send gate,
    in order, which saves a round trip. A string is a basic message:
    `{"reply": "Hi"}` sends `{"kind": "basic", "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
    the kinds' endpoints (`POST /v0/conversations/{conversation_id}/basic`, ...). 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 /v0/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.

    ## Retention and deleting contacts

    Message content and files are kept for your plan's retention period (30 days by
    default). After it, a message's content is removed and the message is no longer
    returned, files are deleted, and events older than it leave the log.

    `DELETE /v0/contacts/{contact_id}` erases one contact at once, for example when
    the person asks you to delete their data or when you delete them in your own
    product: their name and address, the content of every message in their
    conversations, the files in them, their events and pending webhook deliveries,
    and messages still queued to them. It cannot be undone. What stays is only IDs,
    timestamps and the counts that bill and pace your senders. If the person writes
    again, or you send to their address, they are a new contact with a new ID.

    ## Testing with simulated contacts

    A test key can play the end user's side without a phone or a real channel. `POST
    /v0/test/conversations` creates a conversation on `telegram`, `whatsapp`,
    `imessage` or `sms` with a **simulated contact**; `POST /v0/test/conversations/{conversation_id}/inbound`
    writes as that contact (text, or media uploaded with `POST /v0/files`). Each turn
    reaches your agent exactly like a real message: a `message.received` event by
    webhook, on the stream and in the event log, in order. Your agent replies with
    its normal sends, which pass the real send gate (the channel's kinds and
    fields, text limits, edit targets) and end `sent` on your app's simulated
    sender, without touching a network. On Telegram, as with a real bot, nothing reaches a
    contact who has not written yet: a send before the contact's first turn ends
    `failed` (`channel_error`, `channel_code` `403`). `GET /v0/test/conversations/{conversation_id}/transcript`
    shows both sides, oldest first: what was sent is exactly what the contact sees.

    Simulated traffic is free: it does not count against the sandbox allowance,
    since nothing leaves Flow and no real person is reached. Two limits bound it
    instead: an app has at most 500 simulated conversations (a new one past that
    answers `403 permission` with `channel_code` `simulated_conversation_limit`),
    and a key makes at most 120 requests a minute to the `/v0/test/` endpoints
    (`429 rate_limited`), counted on top of its plan's request rate limit. The gate also skips the rules that protect a real number's
    standing with its channel (pacing, the new-contact budget and warm-up, abuse
    throttling), so a test suite can run cases in parallel. Simulated conversations,
    contacts and senders carry `simulated: true` (senders `kind: simulated`), exist
    only in test mode, are never shown to live keys, and follow your plan's
    retention like other test data.

    The SMS rules that concern the contact hold on a simulated SMS conversation as
    on a real number. A start or reply before the contact's first turn needs their
    recorded consent (`consent` on `POST /v0/messages`; otherwise `403 permission`
    with `channel_code` `consent_required`). A turn with the text STOP (or a
    variant) opts the simulated contact out (its `opted_out_at`, `opted_out_via`
    `keyword`; sends answer `403 permission` with `channel_code`
    `contact_opted_out`), and START to the simulated sender lifts it; the turn is
    still delivered as `message.received`, and the transcript shows no automatic
    keyword reply. A simulated SMS sender keeps no quiet hours, so a test's outcome
    never depends on the time of day, and it sends media as MMS. A HIPAA account
    (`Account.hipaa`) can simulate only SMS: creating a simulated `telegram`,
    `whatsapp` or `imessage` conversation, or posting a turn to one made before the
    flag was set, answers `403 permission` with `channel_code`
    `hipaa_channel_not_covered`.

    A simulated WhatsApp conversation keeps WhatsApp's rules. The contact's turns go
    through the same inbound path as a real WhatsApp message, so each turn opens the
    24-hour window, and the send gate applies WhatsApp's rules: outside the window
    (also before the contact's first turn) only a `template` can be sent (`409
    outside_window`); buttons take up to 3 reply buttons (a `list` up to 10 rows);
    reactions and `reply_to` work; edits and unsends answer `422
    unsupported_content`.

    ## 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/v0/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`, `simulate_inbound`, `get_test_transcript`,
    `wait_for_event`, `list_events`, `get_webhook_deliveries`, `replay_event`
    and `explain_error`. A `fk_live_` key gets
    production tools: `send_message`, `reply`, `react`, `edit_message`,
    `unsend_message`, `typing`, `list_conversations` and
    `get_conversation_messages`, plus `whoami` and
    `explain_error`. The tools that send take a `message` that is a kind object,
    as in a webhook reply (a string is a basic message's text). 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`, `param`, `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. Test and live mode are chosen by the API key.
security:
  - bearerAuth: []
paths:
  /v0/stream:
    parameters:
      - $ref: "#/components/parameters/FlowVersion"
    get:
      operationId: openStream
      tags:
        - Stream
      summary: Open the live event stream (WebSocket)
      description: |
        Upgrades to a WebSocket that pushes your app's events as they happen, in log
        order. Pass `after` to resume from the last event you saw: the stream first
        replays everything after it, then goes live, so a reconnect never loses an
        event.

        Every WebSocket text frame is one JSON object (`StreamFrame`). The server
        sends `event` frames; the client may send `send` frames with the same bodies
        as the HTTP send endpoints, and gets one `ack` or `error` frame back for each,
        matched by `ref`. When the server is about to restart it sends a `reconnect`
        frame; reconnect with `after` set to the last event you received.

        Authenticate with the `Authorization` header where your WebSocket client can
        set headers. Where it cannot (a browser's `WebSocket`, and Node's global
        `WebSocket` on the server), offer the key as a WebSocket subprotocol instead:
        offer both `flow` and `flow.key.<api key>`, for example
        `new WebSocket("wss://api.flow.engineer/v0/stream", ["flow", "flow.key." + key])`.
        This works from server-side clients as well as browsers. The server selects
        `flow` and never echoes the key. Offering the key protocol without `flow` is
        refused with a plain `400 invalid_request` answer, without an upgrade. When an `Authorization` header is present it
        takes precedence. Keys are never accepted in the query string, since URLs end
        up in logs. A key used in a browser is visible to whoever uses that page: do
        this only for internal tools or with test keys (`fk_test_`).

        **Refusals arrive on the socket.** Many WebSocket clients (Node's and
        browsers' among them) cannot read the HTTP status of a refused upgrade, so
        a WebSocket request that Flow refuses (a missing, unknown, revoked or
        expired key, a bad parameter, too many streams for the key, a restart) is
        still upgraded: the server sends one `error` frame with the usual error
        body (`type`, `message`, `hint`, `retry_after`, `channel_code`, ...), then
        closes with an application close code of 4000 plus the HTTP status the
        error has elsewhere, and a short reason:

        - `4401`: `authentication`. The key is missing, malformed, unknown, revoked
          or expired (`channel_code` `sandbox_key_expired`). Stop reconnecting
          until you have a working key.
        - `4403`: `permission`. The key may not open this stream. Stop
          reconnecting.
        - `4400`: `invalid_request`, for example a bad `after` or `type`. Fix the
          request; reconnecting unchanged fails again.
        - `4429`: `rate_limited`, for example more open streams than the key may
          hold. Reconnect after `retry_after` seconds.
        - `4500`, `4503`: Flow could not open the stream just now, or is
          restarting. Reconnect after `retry_after` seconds with the same `after`.

        An open stream re-checks its key about once a minute: when the key is
        revoked, the stream sends an `authentication` error frame and closes with
        `4401` too. Other closes: `1012` after a `reconnect` frame (reconnect at
        once with `after`), `1008` after too many rate-limited `send` frames in a
        row, `1001` when pings go unanswered, `1011` when the event log is
        unavailable; reconnect with `after` after any of these. A request without
        a WebSocket upgrade gets the same errors as plain HTTP answers.
      parameters:
        - $ref: "#/components/parameters/After"
        - name: type
          in: query
          required: false
          description: Only events of these types. Repeat the parameter for several.
          style: form
          explode: true
          schema:
            type: array
            maxItems: 20
            items:
              $ref: "#/components/schemas/EventType"
      responses:
        "101":
          description: |
            Switching to the WebSocket protocol. The schema below describes each JSON
            frame on the socket, in either direction. A refused WebSocket request is
            upgraded too, then answered with one `error` frame and a 4000-range close
            code (see the description); the 4xx answers below are what a request
            without an upgrade gets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StreamFrame"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "429":
          $ref: "#/components/responses/RateLimited"
        default:
          $ref: "#/components/responses/Error"
components:
  headers:
    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
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
  parameters:
    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
    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
  responses:
    Error:
      description: An error. `error.type` says which (see `ErrorType`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InvalidRequest:
      description: |
        The request is malformed or a parameter is invalid (`invalid_request`).
        `error.param` names the parameter. Every endpoint answers this for a
        `Flow-Version` header that is not a date this API serves (`param`
        `Flow-Version`), and every `POST` that takes an `Idempotency-Key` for a key
        over 255 characters (`param` `Idempotency-Key`).
      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
    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
    Unauthenticated:
      description: The API key is missing, malformed, unknown, revoked or expired (`authentication`).
      headers:
        WWW-Authenticate:
          description: |
            The authentication scheme, `Bearer realm="api.flow.engineer"`. When the
            request carried a key that is well formed but unknown, revoked or
            expired, it also says `error="invalid_token"`:
            `Bearer realm="api.flow.engineer", error="invalid_token"`. A missing or
            malformed key gets the scheme alone.
          schema:
            type: string
            examples:
              - Bearer realm="api.flow.engineer", error="invalid_token"
      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/v0/sandbox/keys"
              doc_url: https://api.flow.engineer/docs/errors/authentication
  schemas:
    AppId:
      type: string
      description: An app ID, `app_` and a ULID.
      pattern: ^app_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - app_01JB8Z0A1C3E5G7J9K1M3P5R7T
    BasicImessageOptions:
      type: object
      description: |
        iMessage only: styles and a subject line. On another channel the block
        answers `422 unsupported_content` with `param` `imessage`; an unknown key
        `400 invalid_request`. A message that goes by SMS or RCS (see
        `Message.network`) shows neither.
      additionalProperties: false
      properties:
        styles:
          type: array
          description: Styled ranges of `text` (see `ImessageStyle`). Each range must lie within `text`; one that does not answers `400 invalid_request` naming it (`imessage.styles[0]`).
          items:
            $ref: "#/components/schemas/ImessageStyle"
        subject:
          type: string
          minLength: 1
          maxLength: 200
          description: iMessage's subject line, shown in bold above the text.
    BasicKind:
      description: A `basic` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`BasicMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/BasicMessage"
        - required:
            - kind
    BasicMessage:
      type: object
      description: |
        Text, or one media file with an optional caption, in either direction: the
        kind every channel takes (`POST /v0/conversations/{conversation_id}/basic`).
        Give `text`, `media`, or both (then `text` is the caption). A field not
        listed here is refused with `400 invalid_request` naming it in `param`;
        a field or channel block the conversation's channel does not take answers
        `422 unsupported_content` naming it.

        Received from a contact: their text and/or one file. Inbound media always
        has `url` (a Flow file URL, `GET /v0/files/{file_id}`) and `file_id`, on
        every channel (an SMS contact's MMS files included), and `mime_type` and
        `size_bytes` once Flow kept the file; send a received file again by its
        `file_id`. On SMS Flow always keeps its own copy (the provider's is
        deleted): an MMS file it could not fetch or check arrives as
        `file_blocked` instead.
      properties:
        kind:
          type: string
          const: basic
          default: basic
          description: Always `basic`. Optional on `POST /v0/conversations/{conversation_id}/basic`; required where no URL names the kind (webhook replies, the stream, starts, a message's `content`).
        text:
          type: string
          minLength: 1
          maxLength: 9999
          description: |
            The text, sent exactly as written: no markdown, no stripping. At most
            4096 UTF-16 code units on Telegram (an emoji such as 😀 counts as 2),
            4096 characters on WhatsApp, 9999 on iMessage, and 10 segments on SMS
            (see `SmsSegments`). With `media` it is the caption, at most 1024 (UTF-16
            code units on Telegram). Optional when `media` is given. Longer text is
            refused with `400 invalid_request`.
        media:
          $ref: "#/components/schemas/Media"
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        link_preview:
          type: boolean
          description: |
            Whether links in the text show a rich preview. Left out, the channel's
            default applies (Telegram, WhatsApp and iMessage show previews).
            `false` sends links as plain text. SMS never shows previews, and no
            channel shows one on a media message: `false` is accepted there, and
            `true` on SMS or with `media` answers `422 unsupported_content` with
            `param` `link_preview`.
        telegram:
          $ref: "#/components/schemas/BasicTelegramOptions"
        imessage:
          $ref: "#/components/schemas/BasicImessageOptions"
        metadata:
          $ref: "#/components/schemas/Metadata"
    BasicTelegramOptions:
      type: object
      description: |
        Telegram only: how the message looks and notifies on Telegram. On another
        channel the block answers `422 unsupported_content` with `param`
        `telegram`; an unknown key `400 invalid_request`.
      additionalProperties: false
      properties:
        parse_mode:
          $ref: "#/components/schemas/TelegramParseMode"
        disable_notification:
          type: boolean
          description: Deliver silently, with no sound.
        protect_content:
          type: boolean
          description: Stop the contact forwarding or saving the message.
        show_caption_above_media:
          type: boolean
          description: Media only. Show the caption above the photo or video instead of below it.
        has_spoiler:
          type: boolean
          description: Media only (photos and videos). Cover it with a spoiler animation until tapped.
    Button:
      description: A reply button (`id` and `label`) or a link button (`url` and `label`).
      oneOf:
        - $ref: "#/components/schemas/ReplyButton"
        - $ref: "#/components/schemas/UrlButton"
    ButtonReplyMessage:
      type: object
      description: Inbound only. The contact tapped a reply button of a buttons or carousel message, or a row of a list.
      required:
        - kind
        - button_id
        - label
      properties:
        kind:
          type: string
          const: button_reply
          description: Always `button_reply`.
        button_id:
          type: string
          description: The `id` of the tapped button or list row.
        label:
          type: string
          description: The tapped button's label, or the list row's title.
        message_id:
          $ref: "#/components/schemas/MessageId"
    ButtonsKind:
      description: A `buttons` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`ButtonsMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/ButtonsMessage"
        - required:
            - kind
    ButtonsMessage:
      type: object
      description: |
        Text with buttons, sent only (`POST
        /v0/conversations/{conversation_id}/buttons`), optionally under a
        `header`. A tap on a reply button arrives as a `button_reply` message
        carrying the button's `id`. URL buttons open a link and send nothing back.
      required:
        - text
        - buttons
      properties:
        kind:
          type: string
          const: buttons
          default: buttons
          description: Always `buttons`. Optional on its endpoint; required where no URL names the kind.
        header:
          $ref: "#/components/schemas/ContentHeader"
        text:
          type: string
          minLength: 1
          maxLength: 4096
          description: |
            The text above the buttons, sent exactly as written: at most 4096 UTF-16
            code units on Telegram, or 1024 when the message has a media `header`
            (Telegram's caption limit), and 1024 characters on WhatsApp (its limit
            for messages with buttons). Longer text is refused with `400
            invalid_request`.
        buttons:
          type: array
          minItems: 1
          maxItems: 10
          description: |
            The buttons, in order: 1 to 10 on Telegram; on WhatsApp 1 to 3 reply
            buttons or exactly one URL button (anything else answers `422
            unsupported_content` with `param` `buttons`; send a `list` for more
            choices).
          items:
            $ref: "#/components/schemas/Button"
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        telegram:
          $ref: "#/components/schemas/ButtonsTelegramOptions"
        metadata:
          $ref: "#/components/schemas/Metadata"
    ButtonsTelegramOptions:
      type: object
      description: |
        Telegram only: how the message looks and notifies on Telegram. On another
        channel the block answers `422 unsupported_content` with `param`
        `telegram`; an unknown key `400 invalid_request`.
      additionalProperties: false
      properties:
        parse_mode:
          $ref: "#/components/schemas/TelegramParseMode"
        disable_notification:
          type: boolean
          description: Deliver silently, with no sound.
        protect_content:
          type: boolean
          description: Stop the contact forwarding or saving the message.
    Call:
      type: object
      description: One call attempt to an iMessage line.
      required:
        - contact
        - network
      properties:
        contact:
          $ref: "#/components/schemas/ContactId"
          description: The contact who called.
        network:
          type: string
          description: How they called. `facetime` (a FaceTime call) today.
          enum:
            - facetime
          x-enum-varnames:
            - CallNetworkFacetime
    CallData:
      type: object
      description: The call the contact tried to make.
      required:
        - call
      properties:
        call:
          $ref: "#/components/schemas/Call"
    CallForwarding:
      type: object
      description: |
        Dedicated lines only: where phone calls to the line's number are
        forwarded, set up with the Flow team. Forwarded calls send no event.
        FaceTime calls are not forwarded; they arrive as `call.received`. Absent
        when calls are not forwarded (always on shared lines).
      required:
        - phone
      properties:
        phone:
          type: string
          description: The E.164 number calls are forwarded to.
          pattern: ^\+[1-9][0-9]{6,14}$
          examples:
            - "+15555550123"
    CallReceivedEvent:
      description: |
        iMessage only. A contact tried to FaceTime one of your lines. The line
        cannot take calls, so nothing is answered. `conversation` is the direct conversation with the contact, made if it is
        new (with `conversation.started`, `via` `inbound`). A call is not a
        message: it does not count toward plan caps on people, and it does not
        open the conversation for a reply-only line (a send there before the
        contact writes is a start, refused with `403 permission` unless
        `Sender.imessage.can_initiate` is on). Phone calls forwarded by
        `Sender.imessage.call_forwarding` send no event. Delivered only to webhook
        endpoints that list `call.received` in `events`.
      allOf:
        - $ref: "#/components/schemas/EventBase"
        - type: object
          required:
            - type
            - data
            - conversation
          properties:
            type:
              type: string
              enum:
                - call.received
              description: Always `call.received`.
            data:
              $ref: "#/components/schemas/CallData"
    CarouselCard:
      type: object
      description: One card of a carousel.
      required:
        - header
        - buttons
      properties:
        header:
          $ref: "#/components/schemas/ContentHeader"
          description: The card's image or video, a `ContentHeader` of type `image` or `video` only (by `url` or `file_id`); any other type is refused with `400 invalid_request` at `cards[i].header.type`.
        text:
          type: string
          minLength: 1
          maxLength: 160
          description: The card's text, at most 160 characters and 2 line breaks.
        buttons:
          type: array
          minItems: 1
          maxItems: 2
          description: Exactly one URL button, or 1 to 2 reply buttons; the same number and kind on every card. Labels are at most 20 characters.
          items:
            $ref: "#/components/schemas/Button"
    CarouselKind:
      description: A `carousel` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`CarouselMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/CarouselMessage"
        - required:
            - kind
    CarouselMessage:
      type: object
      description: |
        A row of 2 to 10 cards the contact scrolls sideways, under one text, sent
        only (`POST /v0/conversations/{conversation_id}/carousel`; WhatsApp). Each
        card has an image or video, optional text, and its own buttons. A tap on a
        reply button arrives as a `button_reply` message carrying the button's
        `id`; URL buttons open a link and send nothing back. Every card has the
        same number and kind of buttons; a card has either exactly one URL button
        or 1 to 2 reply buttons; reply button `id`s are unique across the
        carousel. Each rule is refused with `400 invalid_request` when broken.
      required:
        - text
        - cards
      properties:
        kind:
          type: string
          const: carousel
          default: carousel
          description: Always `carousel`. Optional on its endpoint; required where no URL names the kind.
        text:
          type: string
          minLength: 1
          maxLength: 1024
          description: The message text shown above the cards, 1 to 1024 characters. Longer text is refused with `400 invalid_request`.
        cards:
          type: array
          minItems: 2
          maxItems: 10
          description: 2 to 10 cards, in the order shown (left to right).
          items:
            $ref: "#/components/schemas/CarouselCard"
        metadata:
          $ref: "#/components/schemas/Metadata"
      examples:
        - text: Three picks for you
          cards:
            - header:
                type: image
                url: https://example.com/a.jpg
              text: Linen shirt, 1,499
              buttons:
                - id: buy_a
                  label: Buy
            - header:
                type: image
                url: https://example.com/b.jpg
              text: Denim jacket, 2,999
              buttons:
                - id: buy_b
                  label: Buy
    Channel:
      type: string
      description: A messaging channel. `sms` is US SMS and MMS (in pilot), from dedicated numbers only.
      enum:
        - telegram
        - whatsapp
        - imessage
        - sms
    Consent:
      type: object
      description: |
        The contact's consent to receive messages from you, as you recorded it
        before starting the conversation. Required to start an SMS conversation
        with someone who has never texted your numbers (US law, the TCPA, needs
        their prior express consent). Flow keeps it on the contact. It does not
        undo an opt-out: only the contact texting START does.
      required:
        - obtained_at
        - method
      properties:
        obtained_at:
          type: string
          format: date-time
          description: When the person gave consent. Not in the future.
        method:
          type: string
          description: How they gave it.
          enum:
            - web_form
            - paper_form
            - verbal
            - text_message
            - app
            - other
        proof:
          type: string
          maxLength: 500
          description: Your own reference to the record of consent (a form submission ID or URL). Do not put health information here.
    ContactAddress:
      type: object
      description: How the contact is reached on their channel. Which fields are set depends on the channel; do not assume `phone`.
      properties:
        phone:
          type: string
          description: E.164 phone number (WhatsApp and SMS; iMessage when the handle is a number).
        username:
          type: string
          description: A username without `@` (Telegram, WhatsApp usernames).
        telegram_user_id:
          type: string
          description: Telegram's numeric user ID, as a string.
        handle:
          type: string
          description: iMessage handle (a phone number or an email address).
    ContactCardKind:
      description: A `contact_card` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`ContactCardMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/ContactCardMessage"
        - required:
            - kind
    ContactCardMessage:
      type: object
      description: |
        A contact card, in either direction (`POST
        /v0/conversations/{conversation_id}/contact-card` sends one; Telegram and
        WhatsApp). Inbound, a card the contact shared.
      required:
        - name
      properties:
        kind:
          type: string
          const: contact_card
          default: contact_card
          description: Always `contact_card`. Optional on its endpoint; required where no URL names the kind.
        name:
          type: string
          minLength: 1
          description: The person's or business's name.
        phones:
          type: array
          description: Phone numbers, E.164 where known. Telegram needs at least one (else `422 unsupported_content`, `param` `phones`).
          items:
            type: string
        emails:
          type: array
          description: Email addresses.
          items:
            type: string
            format: email
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        metadata:
          $ref: "#/components/schemas/Metadata"
    ContactDeletedData:
      type: object
      description: The contact that was deleted. Only the ID is kept.
      required:
        - contact
      properties:
        contact:
          $ref: "#/components/schemas/ContactId"
    ContactDeletedEvent:
      description: |
        A contact was deleted with `DELETE /v0/contacts/{contact_id}`: their address,
        messages, files and events are erased at Flow. Delete what you keep about
        them too. Sent once, for the first delete; it has no `conversation`.
      allOf:
        - $ref: "#/components/schemas/EventBase"
        - type: object
          required:
            - type
            - data
          properties:
            type:
              type: string
              enum:
                - contact.deleted
              description: Always `contact.deleted`.
            data:
              $ref: "#/components/schemas/ContactDeletedData"
    ContactId:
      type: string
      description: A contact ID, `ct_` and a ULID.
      pattern: ^ct_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - ct_01JB8ZB2J4K6N8Q0S2V4W6Y8A0
    ContactLeftData:
      type: object
      description: The contact who left, the shared sender they left, and why.
      required:
        - contact
        - sender
        - reason
      properties:
        contact:
          $ref: "#/components/schemas/ContactId"
        sender:
          $ref: "#/components/schemas/SenderId"
        reason:
          type: string
          enum:
            - joined_other_app
          description: |
            Why the contact left. `joined_other_app`: they sent the join message of
            another app on the shared sender, or of your app's other mode (test or live).
    ContactLeftEvent:
      description: |
        A person on a shared sender (the sandbox) joined another app, or the other
        mode of your app, by sending its join message. Nothing more reaches you from
        them, and sends to them fail with `permission` (`channel_code`
        `sandbox_contact_left`), until they send your join message again, which starts
        a new `conversation.started`. The contact and their history are kept;
        `conversation` is the conversation they left.
      allOf:
        - $ref: "#/components/schemas/EventBase"
        - type: object
          required:
            - type
            - data
            - conversation
          properties:
            type:
              type: string
              enum:
                - contact.left
              description: Always `contact.left`.
            data:
              $ref: "#/components/schemas/ContactLeftData"
    ContentHeader:
      type: object
      description: |
        What is shown above a buttons message's text, or a carousel card's image or
        video: an image, a video, a document or a short title. A media header
        takes exactly one of `url` (an HTTPS URL Flow fetches) or `file_id` (from
        `POST /v0/files`), and is fetched and checked for the channel like a basic
        message's `media` (so WebP and other formats work); a `text` header takes
        `text`. Anything else, or a value over a limit, is refused with `400
        invalid_request` naming the field in `param`.

        WhatsApp shows the header at the top of the same message bubble. Telegram
        sends one message: the media with the text as its caption and the buttons
        under it; a `text` header is the message's first line, in bold.
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - image
            - video
            - document
            - text
          description: What the header holds.
        url:
          type: string
          format: uri
          description: Media headers only. An HTTPS URL of the media, which Flow fetches.
        file_id:
          $ref: "#/components/schemas/FileId"
        text:
          type: string
          minLength: 1
          maxLength: 60
          description: "`text` headers only. The title, 1 to 60 characters (WhatsApp's header limit)."
        filename:
          type: string
          maxLength: 255
          description: "`document` headers only. The file's name, shown with the document."
      examples:
        - type: image
          url: https://example.com/menu.webp
        - type: text
          text: Your order
    Conversation:
      type: object
      description: |
        One sender talking with one contact (`type` `direct`), or with an iMessage
        group chat (`type` `group`). Holds the window state and whether the contact
        opted in.
      required:
        - id
        - app
        - channel
        - sender
        - livemode
        - opted_in
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/ConversationId"
        app:
          $ref: "#/components/schemas/AppId"
        type:
          $ref: "#/components/schemas/ConversationType"
        channel:
          $ref: "#/components/schemas/Channel"
        sender:
          $ref: "#/components/schemas/SenderId"
        contact:
          $ref: "#/components/schemas/ContactId"
          description: The contact the conversation is with. Always set when `type` is `direct`; absent on group conversations (each message's `from` says who wrote).
        group:
          $ref: "#/components/schemas/Group"
        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). Always `true` on group conversations, which begin with the group writing.
        simulated:
          type: boolean
          readOnly: true
          description: "`true` for a simulated conversation, whose contact exists only inside Flow (test mode only, from `POST /v0/test/conversations`); absent otherwise."
        metadata:
          $ref: "#/components/schemas/Metadata"
        created_at:
          type: string
          format: date-time
          description: When the conversation began.
    ConversationId:
      type: string
      description: A conversation ID, `conv_` and a ULID.
      pattern: ^conv_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1
    ConversationRef:
      type: object
      description: The conversation an event happened in, inlined so most handlers need no extra call. A group's participants are not inlined; read the conversation for them.
      required:
        - id
        - channel
        - sender
      properties:
        id:
          $ref: "#/components/schemas/ConversationId"
        type:
          $ref: "#/components/schemas/ConversationType"
        channel:
          $ref: "#/components/schemas/Channel"
        sender:
          $ref: "#/components/schemas/SenderId"
        contact:
          $ref: "#/components/schemas/ContactId"
          description: The contact the conversation is with. Always set when `type` is `direct`; absent on group conversations (the message's `from` says who wrote).
        group_name:
          type: string
          description: Group conversations only. The group chat's name, when it has one.
        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.
        simulated:
          type: boolean
          readOnly: true
          description: "`true` for a simulated conversation, whose contact exists only inside Flow (test mode only, from `POST /v0/test/conversations`); absent otherwise."
    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 message or opened its Telegram link) or `outbound` (you started it)."
          enum:
            - inbound
            - sandbox_join
            - outbound
    ConversationStartedEvent:
      description: |
        A new conversation began. Sent before the first `message.received` of a new
        contact, or, when you started the conversation (`data.via` `outbound`), with
        your first send to the contact. A sandbox join (`data.via` `sandbox_join`) also starts one: 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. Then the join
        message reaches your app as the contact's first `message.received`, after
        this event, with the code taken out: the words before `(ref <code>)`,
        trimmed (`Hi Sandbox app`), or `Hi` when nothing is left; a Telegram link's
        start arrives as `/start`. Answer it like any first message. Joining again
        in a conversation that already exists sends no new `conversation.started`;
        the join message is still delivered as a `message.received`.
      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"
    ConversationType:
      type: string
      description: |
        Flow always sends it; read a missing one as `direct` (answers from
        before groups existed). `direct`: one sender and one contact (every
        channel). `group`: a group chat
        one of your dedicated iMessage lines was added to; it has `group` and no
        `contact`. Flow's shared iMessage line ignores group chats, so they never
        reach your app from it.
      enum:
        - direct
        - group
      default: direct
      x-enum-varnames:
        - ConversationDirect
        - ConversationGroup
    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"
    EditImessageOptions:
      type: object
      description: iMessage only. Styles for the new text. An unknown key is refused with `400 invalid_request`.
      additionalProperties: false
      properties:
        styles:
          type: array
          description: Styled ranges of the new `text` (see `ImessageStyle`).
          items:
            $ref: "#/components/schemas/ImessageStyle"
    EditMessage:
      description: |
        A change to the text of one of your sent messages, as a kind: a piece of a
        webhook reply or on the stream, and the `content` of the message that
        carries an edit (`PATCH /v0/messages/{message_id}` over HTTP, whose rules
        apply). `message_id` names the message to change.
      allOf:
        - $ref: "#/components/schemas/EditMessageRequest"
        - type: object
          required:
            - kind
            - message_id
          properties:
            kind:
              type: string
              const: edit
              description: Always `edit`.
            message_id:
              $ref: "#/components/schemas/MessageId"
    EditMessageRequest:
      type: object
      description: |
        The new text of a sent message (`PATCH /v0/messages/{message_id}`), sent
        exactly as written. A field not listed here is refused with `400
        invalid_request` naming it in `param`; a channel block for another channel
        answers `422 unsupported_content` with `param` the block's name.
      required:
        - text
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 9999
          description: |
            The new text: at most 4096 UTF-16 code units on Telegram (1024 when it
            replaces a media message's caption) and 9999 characters on iMessage.
            Longer text is refused with `400 invalid_request`.
        telegram:
          $ref: "#/components/schemas/EditTelegramOptions"
        imessage:
          $ref: "#/components/schemas/EditImessageOptions"
    EditTelegramOptions:
      type: object
      description: Telegram only. How Telegram reads the new text. An unknown key is refused with `400 invalid_request`.
      additionalProperties: false
      properties:
        parse_mode:
          $ref: "#/components/schemas/TelegramParseMode"
    EffectKind:
      description: An `effect` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`EffectMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/EffectMessage"
        - required:
            - kind
    EffectMessage:
      type: object
      description: Text sent with an iMessage effect, sent only (`POST /v0/conversations/{conversation_id}/effect`; iMessage). Which effects a contact's device shows may vary.
      required:
        - text
        - effect
      properties:
        kind:
          type: string
          const: effect
          default: effect
          description: Always `effect`. Optional on its endpoint; required where no URL names the kind.
        text:
          type: string
          minLength: 1
          maxLength: 9999
          description: The text, sent exactly as written, at most 9999 characters. Longer text is refused with `400 invalid_request`.
        effect:
          type: string
          description: The effect.
          enum:
            - slam
            - loud
            - gentle
            - invisible_ink
            - echo
            - spotlight
            - balloons
            - confetti
            - love
            - lasers
            - fireworks
            - celebration
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        metadata:
          $ref: "#/components/schemas/Metadata"
    Error:
      type: object
      description: The body of every error answer.
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/ErrorBody"
    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
            /v0/conversations/{conversation_id}/template." For `unsupported_content`
            it says what to send instead over plain HTTP. Written for developers and
            coding agents alike. Do not parse it; it may be reworded at any time.
          examples:
            - "Send a template instead: POST /v0/conversations/{conversation_id}/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 (`kind`, `telegram.is_big`, `buttons`, `cards[1].header.type`, `limit`).
        retry_after:
          type: integer
          minimum: 0
          description: Seconds to wait before retrying, for errors that clear by themselves.
        channel:
          $ref: "#/components/schemas/Channel"
          description: The conversation's channel, on errors about what it takes (`unsupported_content`) and its windows (`outside_window`).
        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
            /v0/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).
            SMS: `contact_opted_out` and `consent_required` (`permission`),
            `quiet_hours` (`outside_window`, with `retry_after`), and
            `carrier_filtered`, `unreachable`, `invalid_number`, `landline`,
            `registration_required`, `carrier_error` and `provider_not_configured`
            (`channel_error`). HIPAA accounts: `hipaa_channel_not_covered`
            (`permission`). Connecting your own WhatsApp number (`POST
            /v0/senders`): `whatsapp_token_malformed`, `whatsapp_token_rejected`,
            `whatsapp_app_secret_malformed` and `whatsapp_number_not_in_account`
            (`invalid_request`), and `whatsapp_number_taken`,
            `whatsapp_account_taken` and `whatsapp_number_managed` (`permission`).
            Simulated conversations (`permission`): `test_mode_only`,
            `not_simulated`, `simulated_contact` and `simulated_conversation_limit`.
            For a message that failed after it was queued (`message.failed`):
            `sandbox_contact_left` (`permission`), `window_closed` and
            `queued_too_long` (`outside_window`), and
            `channel_timeout_unknown_outcome` (`channel_error`: the channel never
            confirmed the send, so it may or may not have arrived). iMessage
            (`unsupported_content`): `group_unsupported` (a reaction, typing or a
            read receipt in a group conversation; `param` `kind`) and
            `not_on_sms` (an edit or unsend of a message that went by SMS or RCS;
            `param` `kind`). SMS (`unsupported_content`): `no_mms` (media to a
            number without MMS; `param` `media`).
            Edits and unsends (`unsupported_content`): `edit_window_closed` and
            `unsend_window_closed` (the message is older than the channel allows
            for the change) and `first_message_not_editable` (iMessage: the message
            that started the conversation). From typing and
            read receipts, `unknown_outcome` (`channel_error`: the call timed out after
            it was sent).
        request_id:
          type: string
          description: Flow's ID for this request. Quote it when asking for help.
    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, or 413 for a request body over the size limit): the request is malformed or a parameter is invalid. Fix the parameter named in `param`; the `hint` says what it must look like. Every endpoint answers it for a `Flow-Version` header that is not a version this API serves.
        - `authentication` (401): the API key is missing, malformed, unknown, revoked or expired. An `Authorization: Bearer` header with nothing after it usually means the environment variable holding the key was empty when the header was built (for an MCP client, when the server was added): the `hint` says so. Send `Authorization: Bearer fk_test_...` or `fk_live_...` with a current key; with no key at all, get a test key with `POST /v0/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 message, wait for the contact to message the line first, or sign in through the device flow to lift the allowance. On SMS: `contact_opted_out` (the contact texted STOP; nothing more goes to them until they text START), `consent_required` (a start to someone who never texted your numbers: record their consent with `consent` on the start). In a HIPAA account: `hipaa_channel_not_covered` (the channel has no business associate agreement; use SMS). Simulated conversations: `simulated_contact` (a real sender was pointed at a simulated contact; reply in the contact's simulated conversation), `not_simulated` (a simulated sender or test endpoint was pointed at a real contact), `test_mode_only`, `simulated_conversation_limit`. In `message.failed`: `sandbox_contact_left` (the contact joined another app on the sandbox sender while the message was queued; have them send your join message again).
        - `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, connecting a WhatsApp number; a secret that was lost must be rotated again, and a WhatsApp number connected again to see its verify token).
        - `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 /v0/conversations/{conversation_id}/template`, or `POST /v0/messages` with `kind` `template` to start), 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 for them is closed (iMessage typing works only within 5 minutes of the contact's last message; WhatsApp read receipts go at any time); ignore it and send your reply. On SMS, `channel_code` `quiet_hours`: the contact has not texted in the last hour and it is inside the sender's quiet hours in their time zone; retry after `retry_after` seconds, or wait for them to text. In `message.failed`: `window_closed` (WhatsApp's 24-hour window closed while the message was queued; send a template) and `queued_too_long` (the message waited in the queue longer than its sender allows, for example while the sender was paused; send it again if it still applies).
        - `unsupported_content` (422): the conversation's channel does not take this kind, field, block or value, and nothing was sent; Flow never converts a message. `param` names what was refused: `kind` (the endpoint's kind is not on this channel, such as a carousel on Telegram), a field or block (`reply_to` on SMS, `telegram` on WhatsApp, `telegram.is_big`), or the field whose value is not taken (`emoji` outside Telegram's reaction emoji, `buttons` for more than 3 WhatsApp buttons). `channel` is the conversation's channel and `hint` says what to send instead over plain HTTP; each endpoint's page lists its channels. In an iMessage group conversation, a reaction, typing or a read receipt answers it with `param` `kind` and `channel_code` `group_unsupported`: leave it out. Media to an SMS number without MMS answers it with `param` `media` and `channel_code` `no_mms`. An edit or unsend of an iMessage message that went by SMS or RCS (its `network`) answers it with `channel_code` `not_on_sms`: send a new message instead. An edit or unsend past the channel's time limit answers it with `channel_code` `edit_window_closed` or `unsend_window_closed` (iMessage: edits within 15 minutes of sending, unsends within 2 minutes; Telegram: unsends within 48 hours), and an iMessage edit of the message that started the conversation with `first_message_not_editable`; `message` states the limit and how long ago the message was sent. Send a new message instead. `POST /v0/files` answers it for a type Flow does not take (programs, web pages, unknown types). A send by `file_id` of a document a contact sent before Flow malware-scanned documents answers it too: Flow sends only documents it scanned.
        - `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. (When the scan cannot run at all, the upload answers a retryable `500 api_error` with `Retry-After` instead.)
        - `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. SMS codes are Flow's, the same whichever carrier network carries the message: `carrier_filtered` (a carrier's spam filter blocked it: make it read like a person's message, no shortened links), `unreachable`, `invalid_number`, `landline`, `registration_required` (the number's 10DLC or toll-free registration is not approved yet), `carrier_error`, and `provider_not_configured` (the SMS sender is not fully set up on Flow's side, so nothing was sent; contact the Flow team). Flow's own codes on every channel: `channel_timeout_unknown_outcome` (in `message.failed`: the channel did not answer after the message was sent, and the resends its policy allows did not settle it, so the message may have arrived or not; check with the contact before sending it again) and `unknown_outcome` (from typing and read receipts: the call timed out after it was sent).
        - `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
    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`, `template.status_changed` and
        `contact.deleted`).
      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"
        - $ref: "#/components/schemas/ContactDeletedEvent"
        - $ref: "#/components/schemas/ContactLeftEvent"
        - $ref: "#/components/schemas/CallReceivedEvent"
      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"
          contact.deleted: "#/components/schemas/ContactDeletedEvent"
          contact.left: "#/components/schemas/ContactLeftEvent"
          call.received: "#/components/schemas/CallReceivedEvent"
    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"
    EventId:
      type: string
      description: An event ID, `evt_` and a ULID.
      pattern: ^evt_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - evt_01JB8ZE5N7Q9S1V3X5Z7B9D1E3
    EventType:
      type: string
      description: |
        - `message.received`: the contact sent a message (`data.message.content`, with `kind`: `basic`, `voice`, `button_reply` for a tap on a button or list row, `reaction`, `location`, `contact_card` or `file_blocked`).
        - `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, a sandbox join (`via` `sandbox_join`: Flow itself confirms the join, then the join message reaches you as the contact's first `message.received`), or your first send to a new contact (`via` `outbound`).
        - `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.
        - `contact.deleted`: a contact was deleted with `DELETE /v0/contacts/{contact_id}`. Carries only the contact's ID; delete what you keep about them too.
        - `contact.left`: a person on a shared sender (the sandbox) joined another app, or the other mode of your app, by sending its join message. Nothing more reaches you from them, and sends to them fail with `sandbox_contact_left`, until they send your join message again.
        - `call.received`: iMessage only. A contact tried to FaceTime the line. The line cannot answer it, and the call does not count as the contact writing: a reply-only line can message them only after they write.
      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
        - contact.deleted
        - contact.left
        - call.received
    FileBlockedMessage:
      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, and one Flow could not scan arrives with `reason`
        `scan_failed`.
      required:
        - kind
        - reason
      properties:
        kind:
          type: string
          const: 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`."
        media_type:
          type: string
          enum:
            - image
            - video
            - document
            - audio
          description: What kind of file 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.
    FileId:
      type: string
      description: A file ID, `file_` and a ULID.
      pattern: ^file_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - file_01JB8ZG7Q9S1V3X5Z7B9D1F3G5
    Group:
      type: object
      readOnly: true
      description: |
        Group conversations only (iMessage): the group chat as Flow saw it last.
        People who join or leave change `participants` without an event; Flow
        updates the list when the group next writes. The channel's own group ID is
        never exposed: use the conversation's ID.
      required:
        - participants
      properties:
        name:
          type: string
          description: The group chat's name, when it has one.
        participants:
          type: array
          description: The people in the group other than your line, each a contact on the channel (made the first time Flow sees them, like a one-to-one contact).
          items:
            $ref: "#/components/schemas/GroupParticipant"
    GroupParticipant:
      type: object
      description: One person in a group chat.
      required:
        - contact
        - address
      properties:
        contact:
          $ref: "#/components/schemas/ContactId"
        address:
          $ref: "#/components/schemas/ContactAddress"
        name:
          type: string
          description: The person's display name, as the channel reports it. May be missing.
    ImessageSender:
      type: object
      readOnly: true
      description: |
        iMessage senders only: what the line can do beyond replying in iMessage.
        Set by the Flow team (ask them to change it); absent on other channels.
      required:
        - fallback
        - can_initiate
      properties:
        fallback:
          type: boolean
          description: |
            Whether messages to a contact who cannot get iMessage go by SMS or RCS
            instead. The network is chosen for each message when it is sent, never
            by your app; the message's `network` says which one carried it.
            Included on Flow's shared lines; on a dedicated line, an add-on
            arranged with the Flow team. Off, a message to a contact who is not on
            iMessage fails (`message.failed`). A message that goes by SMS or RCS
            shows no `imessage.styles`, `imessage.subject` or effect, and edits
            and unsends of it answer `422 unsupported_content` with
            `channel_code` `not_on_sms` (see `Message.network`).
        call_forwarding:
          $ref: "#/components/schemas/CallForwarding"
        can_initiate:
          type: boolean
          description: |
            Whether the line may write first: start a conversation with
            `POST /v0/messages` with a contact who has not messaged it. Off (the
            default), the line only replies and a start answers `403 permission`.
            Writing first is available on request from the Flow team, on dedicated
            lines only (always `false` on shared lines); a line that starts
            conversations spends its new-contact budget (`limits`), which grows
            during warm-up.
    ImessageStyle:
      type: object
      description: One styled range of an iMessage text.
      required:
        - start
        - length
        - style
      properties:
        start:
          type: integer
          minimum: 0
          description: Where the range starts, in UTF-16 code units from the start of `text`.
        length:
          type: integer
          minimum: 1
          description: How long the range is, in UTF-16 code units.
        style:
          type: string
          description: The style. `big` and `small` set the text's size; `shake`, `nod`, `explode`, `ripple`, `bloom` and `jitter` animate it.
          enum:
            - bold
            - italic
            - underline
            - strikethrough
            - big
            - small
            - shake
            - nod
            - explode
            - ripple
            - bloom
            - jitter
    ListKind:
      description: A `list` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`ListMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/ListMessage"
        - required:
            - kind
    ListMessage:
      type: object
      description: |
        WhatsApp's list message, sent only (`POST
        /v0/conversations/{conversation_id}/list`): the text, a button that opens
        the list, and up to 10 rows in sections. A tap on a row arrives as a
        `button_reply` message whose `button_id` is the row's `id` and whose
        `label` is its title. Row `id`s are unique across the list; a repeated one
        is refused with `400 invalid_request`.
      required:
        - text
        - button
        - sections
      properties:
        kind:
          type: string
          const: list
          default: list
          description: Always `list`. Optional on its endpoint; required where no URL names the kind.
        header:
          type: string
          minLength: 1
          maxLength: 60
          description: A title shown above the text, 1 to 60 characters.
        text:
          type: string
          minLength: 1
          maxLength: 4096
          description: The message's text, sent exactly as written, 1 to 4096 characters.
        button:
          type: string
          minLength: 1
          maxLength: 20
          description: The label of the button that opens the list, 1 to 20 characters.
        sections:
          type: array
          minItems: 1
          maxItems: 10
          description: 1 to 10 sections, in order, with at most 10 rows in all.
          items:
            $ref: "#/components/schemas/ListSection"
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        metadata:
          $ref: "#/components/schemas/Metadata"
    ListRow:
      type: object
      description: One option of a list.
      required:
        - id
        - title
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 200
          description: Your ID for the row, returned in `button_reply` as `button_id`. 1 to 200 characters, unique across the list.
        title:
          type: string
          minLength: 1
          maxLength: 24
          description: The row's title, 1 to 24 characters, returned in `button_reply` as `label`.
        description:
          type: string
          minLength: 1
          maxLength: 72
          description: A line under the title, at most 72 characters.
    ListSection:
      type: object
      description: One section of a list.
      required:
        - rows
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 24
          description: The section's title, 1 to 24 characters. Required when the list has more than one section.
        rows:
          type: array
          minItems: 1
          maxItems: 10
          description: The section's rows, in order.
          items:
            $ref: "#/components/schemas/ListRow"
    LocationKind:
      description: A `location` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`LocationMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/LocationMessage"
        - required:
            - kind
    LocationMessage:
      type: object
      description: |
        A place, in either direction (`POST
        /v0/conversations/{conversation_id}/location` sends one; Telegram and
        WhatsApp). Inbound, a location the contact shared.
      required:
        - lat
        - lng
      properties:
        kind:
          type: string
          const: location
          default: location
          description: Always `location`. Optional on its endpoint; required where no URL names the kind.
        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.
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        metadata:
          $ref: "#/components/schemas/Metadata"
    Media:
      type: object
      description: |
        One image, video, document or audio file. When sending, give exactly one of
        `url` (an HTTPS URL Flow fetches; Flow neither stores nor scans it, and on
        Telegram a document by URL works for PDF and ZIP files) or `file_id` (from
        `POST /v0/files`). Each channel takes files up to its own size: Telegram
        50 MB, WhatsApp and iMessage 100 MB, SMS (MMS) 5 MB; a larger file answers
        `422 unsupported_content` with `param` `media`. SMS carries images, audio
        and video, never documents (`param` `media.type`), and only from numbers
        with MMS (`channel_code` `no_mms`).
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - image
            - video
            - document
            - audio
          description: What kind of file it is.
          x-enum-varnames:
            - MediaImage
            - MediaVideo
            - MediaDocument
            - MediaAudio
        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"
        filename:
          type: string
          maxLength: 255
          description: "The file's name, shown for documents and taken only with `type` `document` (on another type it answers `422 unsupported_content` with `param` `media.filename`). On Telegram it works only with `file_id`: Telegram names a document sent by `url` itself, so `filename` with `url` answers `422 unsupported_content` with `param` `media.filename`."
        mime_type:
          type: string
          readOnly: true
          description: Inbound only. The real type, checked from the file's first bytes.
        size_bytes:
          type: integer
          readOnly: true
          description: Inbound only. The file's size.
    Message:
      type: object
      description: |
        One message in or out of a conversation, with its kind and content and its
        delivery status. What you send is what the contact gets: Flow never
        converts a message into another kind.
      required:
        - id
        - conversation
        - direction
        - status
        - content
        - livemode
        - created_at
      properties:
        id:
          $ref: "#/components/schemas/MessageId"
        conversation:
          $ref: "#/components/schemas/ConversationId"
        direction:
          $ref: "#/components/schemas/MessageDirection"
        from:
          $ref: "#/components/schemas/ContactId"
          readOnly: true
          description: Inbound messages only, on every channel. The contact who wrote it; in a group conversation, which participant. Absent on your own messages.
        status:
          $ref: "#/components/schemas/MessageStatus"
        content:
          $ref: "#/components/schemas/MessageContent"
        network:
          $ref: "#/components/schemas/MessageNetwork"
        error:
          $ref: "#/components/schemas/ErrorBody"
        channel_message_id:
          type: string
          description: The channel's own ID for the message, once the channel accepted it.
        sms:
          $ref: "#/components/schemas/SmsSegments"
        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 or WhatsApp 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.
    MessageContent:
      type: object
      description: |
        A message's content: one kind object, in either direction, with `kind`
        (always present here) saying which. It is exactly what was sent or
        received, except `metadata`, which is kept once, at the message's own
        `metadata`, and not repeated here.
      required:
        - kind
      properties:
        kind:
          $ref: "#/components/schemas/MessageKind"
      oneOf:
        - $ref: "#/components/schemas/BasicKind"
        - $ref: "#/components/schemas/VoiceKind"
        - $ref: "#/components/schemas/ButtonsKind"
        - $ref: "#/components/schemas/ListKind"
        - $ref: "#/components/schemas/CarouselKind"
        - $ref: "#/components/schemas/TemplateKind"
        - $ref: "#/components/schemas/ContactCardKind"
        - $ref: "#/components/schemas/LocationKind"
        - $ref: "#/components/schemas/EffectKind"
        - $ref: "#/components/schemas/ReactionKind"
        - $ref: "#/components/schemas/TypingKind"
        - $ref: "#/components/schemas/ReadKind"
        - $ref: "#/components/schemas/EditMessage"
        - $ref: "#/components/schemas/UnsendMessage"
        - $ref: "#/components/schemas/ButtonReplyMessage"
        - $ref: "#/components/schemas/FileBlockedMessage"
      discriminator:
        propertyName: kind
        mapping:
          basic: "#/components/schemas/BasicKind"
          voice: "#/components/schemas/VoiceKind"
          buttons: "#/components/schemas/ButtonsKind"
          list: "#/components/schemas/ListKind"
          carousel: "#/components/schemas/CarouselKind"
          template: "#/components/schemas/TemplateKind"
          contact_card: "#/components/schemas/ContactCardKind"
          location: "#/components/schemas/LocationKind"
          effect: "#/components/schemas/EffectKind"
          reaction: "#/components/schemas/ReactionKind"
          typing: "#/components/schemas/TypingKind"
          read: "#/components/schemas/ReadKind"
          edit: "#/components/schemas/EditMessage"
          unsend: "#/components/schemas/UnsendMessage"
          button_reply: "#/components/schemas/ButtonReplyMessage"
          file_blocked: "#/components/schemas/FileBlockedMessage"
    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"
    MessageDirection:
      type: string
      description: "`in` from the contact, `out` from your app."
      enum:
        - in
        - out
    MessageEventData:
      type: object
      description: The message the event is about.
      required:
        - message
      properties:
        message:
          $ref: "#/components/schemas/Message"
    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"
    MessageId:
      type: string
      description: A message ID, `msg_` and a ULID.
      pattern: ^msg_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2
    MessageKind:
      type: string
      description: |
        The kinds of message. A closed list: a new kind arrives only with a new API
        version. Each kind has its own schema (`BasicMessage`, `VoiceMessage`, ...)
        and, where it can be sent over HTTP, its own endpoint of the same name
        (`POST /v0/conversations/{conversation_id}/<kind>`, with `contact-card` for
        `contact_card`). `edit` and `unsend` are sent over HTTP with `PATCH` and
        `DELETE /v0/messages/{message_id}`, and are kinds in webhook replies, on
        the stream and in a message's `content`. `button_reply` and `file_blocked`
        are inbound only.

        Received from a contact: `basic`, `voice`, `button_reply`, `reaction`,
        `location`, `contact_card` and `file_blocked`. Sent: every kind but
        `button_reply` and `file_blocked`.
      enum:
        - basic
        - voice
        - buttons
        - list
        - carousel
        - template
        - contact_card
        - location
        - effect
        - reaction
        - typing
        - read
        - edit
        - unsend
        - button_reply
        - file_blocked
    MessageNetwork:
      type: string
      readOnly: true
      description: |
        iMessage channel only: the network that carried the message. `imessage`,
        or `sms` or `rcs` when the line has the SMS/RCS add-on
        (`Sender.imessage.fallback`) and the contact could not get iMessage. The
        network is chosen for each message when it is sent, never by your app.
        Set on inbound messages, and on your messages once the channel reports it
        (in `message.sent`, `message.delivered` or `message.failed`, as
        `data.message.network`); absent before that and on other channels. A
        message that went by `sms` or `rcs` showed no `imessage.styles`, no
        `imessage.subject` and no effect, and edits and unsends of it answer `422
        unsupported_content` with `channel_code` `not_on_sms`.
      enum:
        - imessage
        - sms
        - rcs
      x-enum-varnames:
        - NetworkImessage
        - NetworkSms
        - NetworkRcs
    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"
    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"
    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
    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
    OutboundKind:
      type: object
      description: |
        One message to send where no URL names the kind: a piece of a webhook
        reply (`WebhookReply`) or the `message` of a stream `send` frame. `kind`
        is required and selects the shape; the fields and channel rules are the
        same as on the kind's endpoint (`edit` and `unsend` as on `PATCH` and
        `DELETE /v0/messages/{message_id}`, with the target in `message_id`).
      required:
        - kind
      oneOf:
        - $ref: "#/components/schemas/BasicKind"
        - $ref: "#/components/schemas/VoiceKind"
        - $ref: "#/components/schemas/ButtonsKind"
        - $ref: "#/components/schemas/ListKind"
        - $ref: "#/components/schemas/CarouselKind"
        - $ref: "#/components/schemas/TemplateKind"
        - $ref: "#/components/schemas/ContactCardKind"
        - $ref: "#/components/schemas/LocationKind"
        - $ref: "#/components/schemas/EffectKind"
        - $ref: "#/components/schemas/ReactionKind"
        - $ref: "#/components/schemas/TypingKind"
        - $ref: "#/components/schemas/ReadKind"
        - $ref: "#/components/schemas/EditMessage"
        - $ref: "#/components/schemas/UnsendMessage"
      discriminator:
        propertyName: kind
        mapping:
          basic: "#/components/schemas/BasicKind"
          voice: "#/components/schemas/VoiceKind"
          buttons: "#/components/schemas/ButtonsKind"
          list: "#/components/schemas/ListKind"
          carousel: "#/components/schemas/CarouselKind"
          template: "#/components/schemas/TemplateKind"
          contact_card: "#/components/schemas/ContactCardKind"
          location: "#/components/schemas/LocationKind"
          effect: "#/components/schemas/EffectKind"
          reaction: "#/components/schemas/ReactionKind"
          typing: "#/components/schemas/TypingKind"
          read: "#/components/schemas/ReadKind"
          edit: "#/components/schemas/EditMessage"
          unsend: "#/components/schemas/UnsendMessage"
      examples:
        - kind: basic
          text: Here are the sizes we have.
        - kind: buttons
          text: Which size?
          buttons:
            - id: size_s
              label: Small
            - id: size_m
              label: Medium
    QuietHours:
      type: object
      description: |
        The hours, in the contact's local time zone (worked out from their area
        code; when it is not known, every US time zone must be outside the
        hours), in which the sender does not message contacts who have not texted
        it in the last hour. Absent when the sender has no quiet hours. A send
        inside them answers `409 outside_window` with `channel_code` `quiet_hours`
        and `retry_after`; a message that waited in the queue into them is held
        until they end. Replies to a contact who texted within the hour always
        go.
      required:
        - start
        - end
      properties:
        start:
          type: string
          description: When the quiet hours begin, `HH:MM` on a 24-hour clock.
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          examples:
            - 21:00
        end:
          type: string
          description: When they end, `HH:MM` on a 24-hour clock (earlier than `start` when they span midnight).
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          examples:
            - 08:00
    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"
    ReactionEventData:
      type: object
      description: A reaction the contact added or removed.
      required:
        - message_id
        - emoji
      properties:
        from:
          $ref: "#/components/schemas/ContactId"
          description: The contact who reacted, where the channel reports it (in a group conversation, which participant).
        message_id:
          $ref: "#/components/schemas/MessageId"
        emoji:
          type: string
          description: The emoji (iMessage tapbacks are mapped to their closest emoji).
    ReactionKind:
      description: A `reaction` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`ReactionMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/ReactionMessage"
        - required:
            - kind
    ReactionMessage:
      type: object
      description: |
        A reaction to a message, in either direction (`POST
        /v0/conversations/{conversation_id}/reaction` sends one). Set `emoji` to
        `null` to remove your reaction. Inbound reactions also arrive as
        `reaction.added` and `reaction.removed` events.
      required:
        - message_id
        - emoji
      properties:
        kind:
          type: string
          const: reaction
          default: reaction
          description: Always `reaction`. Optional on its endpoint; required where no URL names the kind.
        message_id:
          $ref: "#/components/schemas/MessageId"
        emoji:
          type:
            - string
            - "null"
          maxLength: 16
          description: One emoji, or `null` to remove the reaction. On Telegram only its reaction emoji (listed on the endpoint); any other answers `422 unsupported_content` with `param` `emoji`.
        telegram:
          $ref: "#/components/schemas/ReactionTelegramOptions"
    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"
    ReactionTelegramOptions:
      type: object
      description: |
        Telegram only. On another channel the block answers `422
        unsupported_content` with `param` `telegram`; an unknown key `400
        invalid_request`.
      additionalProperties: false
      properties:
        is_big:
          type: boolean
          description: Show the reaction with a large animation.
    ReadKind:
      description: A `read` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`ReadMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/ReadMessage"
        - required:
            - kind
    ReadMessage:
      type: object
      description: |
        A read receipt, sent only (`POST /v0/conversations/{conversation_id}/read`;
        a `read` piece in a webhook reply or on the stream does the same).
      properties:
        kind:
          type: string
          const: read
          default: read
          description: Always `read`. Optional on its endpoint; required where no URL names the kind.
        up_to:
          $ref: "#/components/schemas/MessageId"
          description: WhatsApp only. The latest inbound message to mark read (default the latest inbound message). iMessage marks the whole conversation read, so `up_to` there answers `422 unsupported_content` with `param` `up_to`.
    Recipient:
      type: object
      description: |
        Who to start a conversation with: an existing `contact`, or an address on the
        sender's channel. Give exactly one field.
      minProperties: 1
      maxProperties: 1
      properties:
        contact:
          $ref: "#/components/schemas/ContactId"
        phone:
          type: string
          description: WhatsApp, iMessage or SMS. E.164 phone number; on SMS a US or Canadian number, `+1` and ten digits.
          pattern: ^\+[1-9][0-9]{6,14}$
        telegram_user_id:
          type: string
          description: Telegram. The user's numeric ID; they must have started the bot.
        handle:
          type: string
          description: iMessage. A phone number or an email address.
    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` as `button_id`: 1 to 64
            bytes (Telegram's limit for a button's data), so at most 64 ASCII
            characters and fewer for other characters. Labels may repeat; the ID is
            what tells the taps apart, so buttons that share a label (a "Details" on
            every card) get their own IDs, such as `details:listing-17`.
        label:
          type: string
          minLength: 1
          maxLength: 20
          description: The button's text. WhatsApp shows at most 20 characters.
    ReplyTo:
      $ref: "#/components/schemas/MessageId"
      description: |
        Send this as an inline reply to an earlier message in the same
        conversation (Telegram, WhatsApp and iMessage show it quoted). SMS has no
        inline replies: there it answers `422 unsupported_content` with `param`
        `reply_to`. Refused with `404 not_found` (`param` `reply_to`) when the
        target is not a message in this conversation, and with `400
        invalid_request` (`param` `reply_to`) when the target never reached the
        channel (for example, it failed). If the target fails after the send was
        accepted, the reply goes out as a normal message.
    Sender:
      type: object
      description: |
        What your agent talks from: a Telegram bot, a WhatsApp number, an iMessage
        line or a US SMS number. `shared` senders are Flow's sandbox, used by many apps in test mode;
        `dedicated` senders are yours alone; `simulated` senders exist only in test
        mode, for simulated conversations. 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:
          $ref: "#/components/schemas/SenderKind"
        livemode:
          type: boolean
          description: "`false` for sandbox and simulated 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
        sms:
          $ref: "#/components/schemas/SmsSender"
        imessage:
          $ref: "#/components/schemas/ImessageSender"
        setup:
          $ref: "#/components/schemas/SenderSetup"
        join_code:
          type: string
          description: |
            Shared senders only. The whole message a contact sends to this sender to join your app: `Hi `, the app's name (`there` when it has none), then the app's `sandbox_join_code` (from `GET /v0/app`) in brackets after `ref`, for example `Hi Sandbox app (ref brave-otter-40718263)`. Show it to testers as is; a link that fills it in (wa.me, sms:) carries it URL-encoded. Any message of at most 160 characters that ends with `(ref <code>)` joins, so a tester may change the words before the brackets.
          examples:
            - Hi Sandbox app (ref brave-otter-40718263)
        created_at:
          type: string
          format: date-time
          description: When the sender was created.
    SenderAddress:
      type: object
      description: How contacts reach the sender. Which fields are set depends on the channel.
      properties:
        phone:
          type: string
          description: WhatsApp, iMessage and SMS. 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.
    SenderId:
      type: string
      description: A sender ID, `snd_` and a ULID.
      pattern: ^snd_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T
    SenderKind:
      type: string
      description: |
        `shared` (the sandbox), `dedicated` (yours) or `simulated` (test mode only:
        your app's stand-in for a channel, used by simulated conversations from
        `POST /v0/test/conversations`; it never touches a network).
      enum:
        - shared
        - dedicated
        - simulated
    SenderLimits:
      type: object
      description: The sender's current budget for starting conversations, and how fast it sends.
      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.
        messages_per_second:
          type: number
          description: |
            The sender's pacing: how many messages a second it takes on average,
            replies included, with short bursts allowed above it. A send past it
            answers `429 rate_limited` with `retry_after`. A shared sandbox
            sender's pacing is shared by every app that uses it. Absent for
            simulated senders, which are not paced.
          examples:
            - 1
        whatsapp_tier:
          type: string
          description: WhatsApp only. Meta's messaging tier for the number.
    SenderSetup:
      type: object
      readOnly: true
      description: |
        Only in the answer to `POST /v0/senders` that connected a WhatsApp number:
        what you still set in your own Meta app, which Flow cannot set for you.
        Flow already points the number's webhooks at `callback_url` (Meta's
        phone-number-level override); entering the same URL in your app makes its
        field subscriptions apply. The verify token is shown only in this answer
        (connecting the number again shows it again); keep it like a password.
      required:
        - callback_url
        - verify_token
        - webhook_fields
        - steps
      properties:
        callback_url:
          type: string
          format: uri
          description: The callback URL to enter in your Meta app (App Dashboard > WhatsApp > Configuration > Webhook).
          examples:
            - https://api.flow.engineer/ingress/whatsapp/snd_01J9ZK3N5B6VQ1M2X4C7R8T9WA
        verify_token:
          type: string
          description: The verify token to enter with the callback URL.
        webhook_fields:
          type: array
          description: The webhook fields to subscribe in the same place.
          items:
            type: string
          examples:
            - - messages
              - message_template_status_update
              - phone_number_quality_update
              - account_update
              - business_capability_update
              - template_category_update
              - account_alerts
        steps:
          type: array
          description: The steps in your Meta app, in order, as plain sentences to show a person.
          items:
            type: string
    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 /v0/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 /v0/senders/{sender_id}`), or its bot or line was
          connected to another app.
      enum:
        - pending
        - active
        - warming_up
        - throttled
        - flagged
        - banned
    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").
    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"
    SmsSegments:
      type: object
      readOnly: true
      description: |
        SMS text messages only: how the text travels as SMS. Carriers bill and
        deliver SMS in segments: 160 GSM-7 characters fit one segment (153 each
        when split), but a single character outside GSM-7 (an emoji, curly quotes,
        most non-Latin letters) makes the whole text UCS-2, 70 characters in one
        segment (67 each when split). An SMS send may use at most 10 segments:
        1530 GSM-7 characters (fewer when it uses GSM-7's two-unit characters such
        as `{` or `€`), or 670 when it holds an emoji or other character outside
        GSM-7; a longer text is refused with `400 invalid_request`. Absent on MMS
        and on other channels.
      required:
        - segments
        - encoding
      properties:
        segments:
          type: integer
          minimum: 1
          description: How many SMS segments the text takes.
        encoding:
          type: string
          description: "`gsm7` or `ucs2`."
          enum:
            - gsm7
            - ucs2
    SmsSender:
      type: object
      readOnly: true
      description: |
        SMS senders only: how the number is registered with US carriers and what it
        can do. An SMS number stays `pending` until its registration is approved
        (carriers block traffic from unregistered numbers).
      required:
        - number_type
        - registration_status
        - two_way
        - mms
      properties:
        number_type:
          type: string
          description: "`10dlc` (a local 10-digit number, registered as a brand and campaign) or `toll_free` (a verified toll-free number)."
          enum:
            - 10dlc
            - toll_free
        registration_status:
          type: string
          description: |
            The number's carrier registration (the 10DLC campaign, or toll-free
            verification): `pending` (submitted, not approved yet; the sender is
            `pending` and sends nothing), `approved`, or `rejected` (the sender is
            `flagged`; the Flow team resubmits).
          enum:
            - pending
            - approved
            - rejected
        two_way:
          type: boolean
          description: Whether the number receives texts. Always `true` for senders Flow connects today.
        mms:
          type: boolean
          description: Whether the number sends and receives MMS (images, audio and video). Without it, a basic message with `media` answers `422 unsupported_content` with `param` `media` and `channel_code` `no_mms`.
        quiet_hours:
          $ref: "#/components/schemas/QuietHours"
    StartBasicMessage:
      description: A start whose first message is a basic message (`kind` `basic`).
      allOf:
        - $ref: "#/components/schemas/StartConversationTarget"
        - $ref: "#/components/schemas/BasicMessage"
        - required:
            - kind
    StartConversationRequest:
      description: |
        The first message from one of your senders to a contact: a `basic` or a
        `template` message, with `kind` saying which (required here), plus
        `sender`, `to` and, on SMS, `consent`. A field not listed for the kind is
        refused with `400 invalid_request` naming it in `param`, never ignored; a
        `kind` other than `basic` or `template` answers `422 unsupported_content`
        with `param` `kind`. On iMessage the sender must be able to write first
        (`Sender.imessage.can_initiate`, available on request); otherwise the
        start answers `403 permission`. A start always makes a `direct`
        conversation: there is no way to start a group. There are no scheduled
        sends: to start a conversation later, wait in your own code and send then.
      oneOf:
        - $ref: "#/components/schemas/StartBasicMessage"
        - $ref: "#/components/schemas/StartTemplateMessage"
      discriminator:
        propertyName: kind
        mapping:
          basic: "#/components/schemas/StartBasicMessage"
          template: "#/components/schemas/StartTemplateMessage"
    StartConversationTarget:
      type: object
      description: Where a start goes. Part of every `StartConversationRequest`.
      required:
        - sender
        - to
      properties:
        sender:
          $ref: "#/components/schemas/SenderId"
        to:
          $ref: "#/components/schemas/Recipient"
        consent:
          $ref: "#/components/schemas/Consent"
          description: SMS only. The contact's consent, required when they have never texted any of your SMS numbers; recorded on the contact. On another channel it answers `422 unsupported_content` with `param` `consent`.
    StartTemplateMessage:
      description: A start whose first message is a WhatsApp template (`kind` `template`).
      allOf:
        - $ref: "#/components/schemas/StartConversationTarget"
        - $ref: "#/components/schemas/TemplateMessage"
        - required:
            - kind
    StreamAckFrame:
      type: object
      description: Server to client. A `send` or `start` frame was accepted.
      required:
        - type
        - ref
        - message
      properties:
        type:
          type: string
          enum:
            - ack
          description: Always `ack`.
        ref:
          type: string
          description: The `ref` of the frame being answered.
        message:
          $ref: "#/components/schemas/Message"
    StreamErrorFrame:
      type: object
      description: Server to client. A `send` or `start` frame was refused (with its `ref`), or the stream itself was refused or failed (without `ref`; the server then closes the socket with a 4000-range close code, `4401` for `authentication`, see `GET /v0/stream`).
      required:
        - type
        - error
      properties:
        type:
          type: string
          enum:
            - error
          description: Always `error`.
        ref:
          type: string
          description: The `ref` of the frame being answered, if any.
        error:
          $ref: "#/components/schemas/ErrorBody"
    StreamEventFrame:
      type: object
      description: Server to client. One event from the log.
      required:
        - type
        - event
      properties:
        type:
          type: string
          enum:
            - event
          description: Always `event`.
        event:
          $ref: "#/components/schemas/Event"
    StreamFrame:
      description: |
        One JSON frame on the `/v0/stream` WebSocket. The server sends `event`, `ack`,
        `error` and `reconnect`; the client sends `send` and `start`.
      oneOf:
        - $ref: "#/components/schemas/StreamEventFrame"
        - $ref: "#/components/schemas/StreamAckFrame"
        - $ref: "#/components/schemas/StreamErrorFrame"
        - $ref: "#/components/schemas/StreamReconnectFrame"
        - $ref: "#/components/schemas/StreamSendFrame"
        - $ref: "#/components/schemas/StreamStartFrame"
      discriminator:
        propertyName: type
        mapping:
          event: "#/components/schemas/StreamEventFrame"
          ack: "#/components/schemas/StreamAckFrame"
          error: "#/components/schemas/StreamErrorFrame"
          reconnect: "#/components/schemas/StreamReconnectFrame"
          send: "#/components/schemas/StreamSendFrame"
          start: "#/components/schemas/StreamStartFrame"
    StreamReconnectFrame:
      type: object
      description: Server to client. The server is restarting; reconnect with `after` set to the last event you received. The socket closes after this frame.
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - reconnect
          description: Always `reconnect`.
        after:
          $ref: "#/components/schemas/EventId"
    StreamSendFrame:
      type: object
      description: |
        Client to server. Sends one message into a conversation, the same as the
        endpoint of its `kind` (`POST /v0/conversations/{conversation_id}/<kind>`,
        or `PATCH` and `DELETE /v0/messages/{message_id}` for `edit` and
        `unsend`); `ref` is also the idempotency key. Frames into one conversation
        go out in the order accepted, in line with the conversation's other sends.
        The frame's `type` is the frame's own type; the message's kind is
        `message.kind`.
      required:
        - type
        - ref
        - conversation
        - message
      properties:
        type:
          type: string
          enum:
            - send
          description: Always `send`.
        ref:
          type: string
          minLength: 1
          maxLength: 255
          description: Your ID for this frame, echoed in the answer and used as the idempotency key.
        conversation:
          $ref: "#/components/schemas/ConversationId"
        message:
          $ref: "#/components/schemas/OutboundKind"
    StreamStartFrame:
      type: object
      description: Client to server. Same as `POST /v0/messages` (a `basic` or `template` message with `kind`, `sender` and `to`); `ref` is also the idempotency key.
      required:
        - type
        - ref
        - message
      properties:
        type:
          type: string
          enum:
            - start
          description: Always `start`.
        ref:
          type: string
          minLength: 1
          maxLength: 255
          description: Your ID for this frame, echoed in the answer and used as the idempotency key.
        message:
          $ref: "#/components/schemas/StartConversationRequest"
    TelegramParseMode:
      type: string
      description: |
        How Telegram reads the text: `HTML` or `MarkdownV2`, Telegram's own
        formatting (see Telegram's Bot API, "Formatting options"). Without it the
        text is plain. Text that Telegram cannot parse is refused by Telegram:
        the message fails (`message.failed`, `channel_error`).
      enum:
        - HTML
        - MarkdownV2
      x-enum-varnames:
        - ParseModeHTML
        - ParseModeMarkdownV2
    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.
    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.
        example:
          type: string
          minLength: 1
          maxLength: 2000
          description: |
            For a `url` button whose link ends in `{{1}}`, a realistic sample value
            for the end of the link (what you will pass in `params.buttons` when
            sending), for WhatsApp's review: Flow shows the reviewers the link with
            the sample in place. That link is at most 2000 characters, and the sample
            has no spaces or line breaks. Left out, Flow sends a placeholder sample
            instead. Not taken on other buttons.
          examples:
            - A-1042
        phone:
          type: string
          description: For `phone_number` buttons, the E.164 number.
    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.
    TemplateCardParams:
      type: object
      description: The values for one card of a carousel template.
      required:
        - card_index
      properties:
        card_index:
          type: integer
          minimum: 0
          maximum: 9
          description: The card's position in the template, from 0.
        header:
          $ref: "#/components/schemas/TemplateHeaderParam"
          description: The card's image or video (`kind` `image` or `video`, by `url` or `file_id`).
        body:
          type: array
          description: Values for the card body's `{{1}}`, `{{2}}`, ... in order.
          items:
            type: string
        buttons:
          type: array
          description: Values for the card's buttons that take one (URL suffixes, quick-reply payloads).
          items:
            $ref: "#/components/schemas/TemplateButtonParam"
      examples:
        - card_index: 0
          header:
            kind: image
            url: https://example.com/a.jpg
          body:
            - Linen shirt
          buttons:
            - index: 0
              value: buy_a
    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.
        examples:
          type: array
          maxItems: 100
          description: |
            For a body or a text header with placeholders, a realistic sample value
            for each placeholder, in order (`{{1}}` first). WhatsApp's reviewers read
            the template with these values, and a template whose samples do not look
            real is often rejected or moved to another category, so give them for
            every new template. Exactly one per placeholder; each 1 to 60 characters
            in a header and 1 to 1024 in a body, with no line breaks, tabs or more
            than four spaces in a row. Left out, Flow sends WhatsApp placeholder
            samples (`x1`, `x2`, ...) instead. Not taken on footers, buttons parts,
            media headers or text without placeholders.
          examples:
            - - Maria
              - A-1042
          items:
            type: string
            minLength: 1
            maxLength: 1024
        buttons:
          type: array
          description: For `buttons`, the buttons in order.
          items:
            $ref: "#/components/schemas/TemplateButton"
    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"
    TemplateId:
      type: string
      description: A template ID, `tpl_` and a ULID.
      pattern: ^tpl_[0-9A-HJKMNP-TV-Z]{26}$
      examples:
        - tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V
    TemplateKind:
      description: A `template` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`TemplateMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/TemplateMessage"
        - required:
            - kind
    TemplateMessage:
      type: object
      description: |
        A WhatsApp message template, sent only (`POST
        /v0/conversations/{conversation_id}/template`, or `POST /v0/messages` to
        start a conversation). Required to start a WhatsApp conversation or to
        write after the 24-hour window closed. The template must be `approved`.

        A media card carousel template (marketing category) is sent the same way,
        with one entry per card in `params.cards`. Inside the 24-hour window a
        carousel needs no template: send a `carousel` message.
      required:
        - template_id
        - language
      properties:
        kind:
          type: string
          const: template
          default: template
          description: Always `template`. Optional on its endpoint; required where no URL names the kind (and on `POST /v0/messages`).
        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"
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        metadata:
          $ref: "#/components/schemas/Metadata"
    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"
        cards:
          type: array
          maxItems: 10
          description: Carousel templates only. Values for each card's header, body placeholders and buttons.
          items:
            $ref: "#/components/schemas/TemplateCardParams"
    TemplateStatus:
      type: string
      description: A template's approval status at Meta.
      enum:
        - pending
        - approved
        - rejected
        - paused
        - disabled
    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.
    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"
    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 the event went out to a webhook endpoint. Its value depends on
            where you read it. In a webhook delivery's body, it is when that delivery
            attempt started (each attempt to each endpoint gets its own), unless an
            endpoint had already accepted the event, in which case it is that
            acceptance time. In the event log (`GET /v0/events`,
            `GET /v0/events/{event_id}`, the stream), it is when the first endpoint
            accepted the event, and it is absent until one has (always, for an app
            with no webhook endpoints).
    TypingEventData:
      type: object
      description: Who is typing.
      required:
        - contact
      properties:
        contact:
          $ref: "#/components/schemas/ContactId"
    TypingImessageOptions:
      type: object
      description: |
        iMessage only. On another channel the block answers `422
        unsupported_content` with `param` `imessage`; an unknown key `400
        invalid_request`.
      additionalProperties: false
      properties:
        duration_seconds:
          type: integer
          minimum: 1
          maximum: 60
          description: How long to show the indicator, 1 to 60 seconds, unless a message goes first.
    TypingKind:
      description: A `typing` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`TypingMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/TypingMessage"
        - required:
            - kind
    TypingMessage:
      type: object
      description: |
        The typing indicator, sent only (`POST
        /v0/conversations/{conversation_id}/typing`; a `typing` piece in a webhook
        reply or on the stream does the same). It shows the indicator, which
        clears itself when your next message goes or after a few seconds.
      properties:
        kind:
          type: string
          const: typing
          default: typing
          description: Always `typing`. Optional on its endpoint; required where no URL names the kind.
        imessage:
          $ref: "#/components/schemas/TypingImessageOptions"
    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"
    UnsendMessage:
      type: object
      description: |
        Removes one of your sent messages from the contact's chat, as a kind: a
        piece of a webhook reply or on the stream, and the `content` of the message
        that carries an unsend (`DELETE /v0/messages/{message_id}` over HTTP,
        whose rules apply).
      required:
        - kind
        - message_id
      properties:
        kind:
          type: string
          const: unsend
          description: Always `unsend`.
        message_id:
          $ref: "#/components/schemas/MessageId"
    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.
    VoiceKind:
      description: A `voice` message with `kind` given, as in a message's `content`, a webhook reply or a stream frame (`VoiceMessage` with `kind` required).
      allOf:
        - $ref: "#/components/schemas/VoiceMessage"
        - required:
            - kind
    VoiceMessage:
      type: object
      description: |
        A voice note, in either direction (`POST
        /v0/conversations/{conversation_id}/voice`). When sending, give `url` or
        `file_id` of an audio file. Inbound voice notes always come with the audio
        file (`url`, `file_id`). On iMessage, a received voice note also carries
        a `transcript` when iMessage made one of it; no other channel has one.
      properties:
        kind:
          type: string
          const: voice
          default: voice
          description: Always `voice`. Optional on its endpoint; required where no URL names the kind.
        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"
        reply_to:
          $ref: "#/components/schemas/ReplyTo"
        metadata:
          $ref: "#/components/schemas/Metadata"
        duration_seconds:
          type: number
          readOnly: true
          description: Inbound only. The voice note's length.
        transcript:
          type: string
          readOnly: true
          description: Inbound only, on iMessage, when iMessage made a transcript of the voice note. What was said.
    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.
  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.
````
