# The public Lalexi API, v1 — the contract customers build against.
#
# Source of truth for the docs site (content/docs/api, rendered with fumadocs-openapi). Checked by
# `node site-docs/scripts/check-openapi.mjs` (routes and response shapes against the server's own
# manifests) and linted with `npx --yes @redocly/cli@1.34.5 lint site-docs/openapi/public-api.yaml`.
#
# Layout rules the check script relies on (a dependency-free reader, see the script's header):
# two-space indentation, block mappings, sequences indented under their key, long text only in
# block scalars (`|` / `>`), no multi-line flow collections and no multi-line plain scalars.
openapi: 3.1.0
info:
  title: Lalexi API
  version: 1.0.0
  summary: Conversations, messages, contacts, channels and webhooks of your Lalexi workspace.
  license:
    name: Proprietary
    identifier: LicenseRef-Lalexi-Proprietary
  description: |
    The Lalexi API gives your own systems — a CRM, a website, a back office — the
    conversations, messages, contacts and channels your team works with in Lalexi.
    Every request acts as one person: the owner of the token, with exactly the access
    that person has in the workspace.

    ## Authentication

    Send your personal access token as a Bearer token:

    ```
    Authorization: Bearer <your token>
    ```

    The token is in Lalexi under **Profile → Access Token**. It is yours, not the
    workspace's: an administrator's token sees the whole workspace, an agent's token
    sees the agent's own channels, and everything sent or changed with it is recorded
    as done by you. Resetting the token in the profile makes the old one stop working
    at once. A missing or unknown token answers `401 invalid_api_key`.

    ## Workspace

    If you belong to several workspaces, name one in the `X-Workspace-Id` header of
    every request — `GET /me` lists your workspaces with their ids. With a single
    workspace the header can be left out. Several workspaces and no header answer
    `400 workspace_required`; a workspace you are not a member of answers
    `404 workspace_not_found`; a suspended workspace `403 workspace_suspended`, and a
    workspace with API access turned off `403 api_disabled`.

    ## Identifiers and times

    A conversation's `id` is the number in its Lalexi URL. Every other `id` is the
    object's own number. Times are ISO 8601 in UTC, for example `2026-10-27T09:30:00Z`.

    ## Errors

    Every error has the same body:

    ```json
    {"error": {"code": "validation_failed", "message": "The request is invalid.", "details": {"text": ["is required"]}}}
    ```

    Branch on `code`: the list is closed and documented in the `ErrorCode` schema.
    `message` is for people and may change. `details` is present only when there is
    more to say — which fields failed, the id of an existing contact, the numbers of a
    reached limit.

    ## Pagination

    Lists answer `{"data": [...], "next_cursor": "..."}`. Ask for up to `limit` items
    (1–100, default 25) and pass `next_cursor` back as `cursor` to get the next page;
    `next_cursor` is `null` on the last page. A cursor is opaque: never build or edit
    one. Short lists (channels, agents, teams, labels, custom fields, webhooks) come
    whole, with `next_cursor: null`.

    ## Idempotency

    `POST /conversations/{conversation_id}/messages` and `POST /dialogs` accept an
    `Idempotency-Key` header — any string up to 255 characters, such as a UUID or the
    id of the record in your system. Repeating the request with the same key within
    24 hours sends nothing new and returns the first answer. While the first request
    is still running, a repeat answers `409 idempotency_in_progress`; the same key with
    a different body answers `422 idempotency_key_reused`. Keys are per person, per
    workspace and per endpoint. When the first attempt failed on our side (a 5xx), the
    key is released and the next attempt runs for real.

    ## Rate limits

    - 300 requests a minute per token;
    - 20 `POST /dialogs` a minute per token;
    - 30 `GET /dialogs/probe` a minute per token (each check is a lookup on the messenger
      made through your linked account);
    - 5 `POST /webhooks/{id}/test` a minute per token;
    - 600 requests a minute per IP address.

    Over a limit the answer is `429 rate_limited` with a `Retry-After` header: the
    number of seconds until the current one-minute window ends.

    ## Starting a dialog

    `POST /dialogs` writes first to a phone number (on an e-mail channel, to an
    address). On messenger channels one identical start goes out per minute: the same
    channel, recipient and text again within 60 seconds is refused with
    `409 duplicate` — a guard against double submits. Use `Idempotency-Key` for safe
    retries. A start whose outcome did not come back in time answers `202` with
    `{"status": "unknown"}`: the message may well have been delivered, so look for it
    in the conversation before sending again.

    ## Webhooks

    Register an HTTPS endpoint with `POST /webhooks` and Lalexi POSTs the events you
    subscribe to — only the ones you yourself may see. Every delivery is signed with
    the endpoint's secret:

    1. take the `X-Lalexi-Timestamp` header and the raw request body, exactly as received;
    2. compute HMAC-SHA256 of `{timestamp}.{body}` with the whole secret (including its
       `whsec_` prefix) as the key, hex-encoded;
    3. compare it in constant time with the hex after `sha256=` in `X-Lalexi-Signature`;
    4. refuse a timestamp more than five minutes away from your clock (a replay).

    Answer any 2xx within 5 seconds (the whole delivery, redirects included, is cut off
    at 15 seconds). Anything else is a failed attempt: it is retried
    up to 5 attempts in all, a few minutes apart at most, and an endpoint whose 50
    attempts in a row failed is switched off (`active: false`) until you turn it back
    on with `PATCH /webhooks/{id}`. Deliveries can arrive out of order and, rarely,
    twice: deduplicate on `X-Lalexi-Delivery` (the envelope's `id`), order by the
    objects' `updated_at`.

    ## Compatibility

    Within `v1` changes are additive only: new endpoints, new optional request fields,
    new response fields, new event types, new error codes, new values in open lists
    (channel types, attachment types). Removing or renaming a field or changing its
    type happens only in a new version, `/v2`. Your integration must ignore fields
    and values it does not know.
servers:
  - url: https://app.lalexi.com/v1
    description: Production
security:
  - personalToken: []
tags:
  - name: Account
    description: Who the token belongs to and which workspaces it opens.
  - name: Channels
    description: The channels of the workspace you work in — messenger accounts, bots, e-mail, website chat.
  - name: Directory
    description: The people, teams, labels and custom fields of the workspace.
  - name: Conversations
    description: Conversations you may open, and changes to their status, assignee, team, labels and fields.
  - name: Messages
    description: Reading a conversation and writing into it.
  - name: Contacts
    description: The people your workspace talks to.
  - name: Dialogs
    description: Writing first to someone who has not written to you.
  - name: Connecting numbers
    description: >-
      Connecting, reconnecting and disconnecting messenger numbers and bots; their history and which
      chats come in. Everything here but connecting a new number is for the person who connected it
      or an administrator (`403 forbidden` otherwise) — a number shared with you is yours to read,
      not to change.
  - name: Broadcasts
    description: One message to many numbers, sent from your connected numbers at a pace.
  - name: Usage
    description: Messages sent and where the workspace stands against its limits.
  - name: Webhooks
    description: Your endpoints that receive events, and a test delivery.
  - name: Webhook events
    description: What Lalexi POSTs to your endpoints — one page per event, with the body and the signature headers.
paths:
  /me:
    get:
      tags:
        - Account
      operationId: getMe
      summary: Who am I
      description: |
        The owner of the token and every workspace it opens, with your role in each. Needs no
        `X-Workspace-Id`: this is where an integration learns which values it may send.
      responses:
        '200':
          description: The token's owner and workspaces.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Me'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /workspace:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Account
      operationId: getWorkspace
      summary: Current workspace
      description: The workspace this request works in (from `X-Workspace-Id`, or your only one) and your role in it.
      responses:
        '200':
          description: The workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workspace'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /channels:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Channels
      operationId: listChannels
      summary: List channels
      description: |
        The channels you work in: all of them for an administrator, the agent's own for an agent.
        The whole list comes at once (`next_cursor` is always `null`). A disconnected channel
        hidden from the workspace (`hidden: true`) is listed only with `include_hidden=true`.
      parameters:
        - name: include_hidden
          in: query
          required: false
          description: Also list the channels hidden from the workspace.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: The channels.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Connecting numbers
      operationId: connectChannel
      summary: Connect a number
      description: |
        Starts connecting a messenger number or bot of the `type` you name — what the channel tile
        under **Settings → Channels → Add** does. The answer is `connect_url`: a short-lived Lalexi
        page where the person whose number it is scans the QR code, enters the code from the
        messenger, or pastes the bot token. Open it in a browser (a new tab or a pop-up); start a
        new one for every attempt.

        When the page is done, the channel appears in `GET /channels` with `connected_by` = you, and
        the `channel.created` webhook fires. Any member of the workspace may connect a number;
        `GET /channel-types` says which types this workspace may connect now.

        | HTTP | code | meaning |
        |---|---|---|
        | 201 | | the page to open |
        | 403 | `channel_unavailable` | this type is not open to the workspace yet |
        | 409 | `connection_limit_reached` | the plan's number of connections is used up (`details.limit`, `details.used`) |
        | 422 | `validation_failed` | `type` is not a type you can connect |
        | 503 | `upstream_unavailable` | the messaging service did not answer; try again |

        At most 10 connects and reconnects a minute per key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelConnect'
      responses:
        '201':
          description: The page that connects the number.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectLink'
        '409':
          description: '`connection_limit_reached` — every connection of the plan is in use.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: connection_limit_reached
                  message: The workspace has connected as many numbers as its plan allows.
                  details:
                    limit: 3
                    used: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/ChannelId'
    get:
      tags:
        - Channels
      operationId: getChannel
      summary: Get a channel
      description: One channel, hidden ones included. A channel you do not work in answers `404 not_found`.
      responses:
        '200':
          description: The channel.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags:
        - Connecting numbers
      operationId: updateChannel
      summary: Change a connected number
      description: |
        `note` — your team's label on the number (empty clears it). `hidden` — take a disconnected
        number and its conversations out of the workspace's sight, or bring them back; a connected
        number cannot be hidden (`422 channel_connected`). Messenger numbers only
        (`422 not_supported` otherwise). An administrator may change any number, an agent the numbers they connected.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelUpdate'
      responses:
        '200':
          description: The channel as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{id}/reconnect:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/ChannelId'
    post:
      tags:
        - Connecting numbers
      operationId: reconnectChannel
      summary: Reconnect a number
      description: |
        For a number whose `status` is `reconnect_required` or `disconnected`: a page that connects
        the same account again, like `POST /channels`. When it is done, the same channel's `status`
        becomes `connected` and `channel.status_changed` fires; its id and conversations stay.
        Messenger numbers only (`422 not_supported` otherwise).
      responses:
        '200':
          description: The page that connects the number again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectLink'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{id}/disconnect:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/ChannelId'
    post:
      tags:
        - Connecting numbers
      operationId: disconnectChannel
      summary: Disconnect a number
      description: |
        Unlinks the account from Lalexi. The channel and its conversations stay, with `status:
        disconnected`; nothing more is received or sent. Connect it again with `POST
        /channels/{id}/reconnect`, or hide it with `PATCH /channels/{id}`.
      responses:
        '200':
          description: The channel as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{channel_id}/history-import:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/NestedChannelId'
    get:
      tags:
        - Connecting numbers
      operationId: getHistoryImport
      summary: History import progress
      description: How far the last import of this number got. Reads `idle` when none ran since our last restart.
      responses:
        '200':
          description: The progress.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryImportProgress'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
    post:
      tags:
        - Connecting numbers
      operationId: startHistoryImport
      summary: Import history
      description: |
        Brings the history of the number's chats into the workspace — the chats its chat filter
        lets through (all of them for a freshly connected number). `deep: true` (the default)
        imports everything the messenger still keeps; `false` the recent messages of each chat.
        Re-running adds only what is missing. Bots have no history (`422 not_supported`).
        Counts against the limit of 10 pieces of queued messenger work a minute per key.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HistoryImportStart'
      responses:
        '202':
          description: The import is queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryImportQueued'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{channel_id}/chats:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/NestedChannelId'
    get:
      tags:
        - Connecting numbers
      operationId: listAccountChats
      summary: The number's chats
      description: |
        The chats of the connected account, newest first, to pick a chat filter from. The list is
        fetched in the background: while `status` is `loading`, ask again in a second or two;
        `reload=true` fetches it afresh. Bots have no chat list (`422 not_supported`).
      parameters:
        - name: reload
          in: query
          required: false
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: The chats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountChatList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{channel_id}/chat-filter:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/NestedChannelId'
    get:
      tags:
        - Connecting numbers
      operationId: getChatFilter
      summary: Which chats come in
      description: "The number's chat filter; `mode` is `all` when none was saved."
      responses:
        '200':
          description: The filter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatFilter'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
    put:
      tags:
        - Connecting numbers
      operationId: setChatFilter
      summary: Choose which chats come in
      description: |
        Replaces the number's chat filter. `all` — every chat; `include` — only `chat_ids`;
        `exclude` — every chat but `chat_ids` (ids from `GET /channels/{channel_id}/chats`). The
        history of the chats the new filter lets through is imported right after, so a save counts
        against the limit of 10 pieces of queued messenger work a minute per key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatFilter'
      responses:
        '200':
          description: The filter as saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatFilter'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channels/{channel_id}/chat-filter/allow:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/NestedChannelId'
    post:
      tags:
        - Connecting numbers
      operationId: allowChat
      summary: Let one more chat in
      description: |
        Lets one chat through the filter, by the person's `phone` or the `chat_id`, without changing
        the rest — for an integration that opens a chat on purpose and expects the reply to arrive.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatFilterAllow'
      responses:
        '200':
          description: The chat is let through.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatFilterAllowed'
        '409':
          description: '`channel_disconnected` — the number is not connected.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /channel-types:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Connecting numbers
      operationId: listChannelTypes
      summary: What can be connected
      description: |
        The types `POST /channels` connects, what each can do, and whether this workspace may connect
        it now (`available`). The whole list comes at once.
      responses:
        '200':
          description: The types.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelTypeList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /agents:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Directory
      operationId: listAgents
      summary: List agents
      description: The people of the workspace, sorted by name. The whole list comes at once.
      responses:
        '200':
          description: The people.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /teams:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Directory
      operationId: listTeams
      summary: List teams
      description: The teams of the workspace. The whole list comes at once.
      responses:
        '200':
          description: The teams.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /labels:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Directory
      operationId: listLabels
      summary: List labels
      description: The labels you can put on conversations and contacts, sorted by title. The whole list comes at once.
      responses:
        '200':
          description: The labels.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LabelList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /custom-fields:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Directory
      operationId: listCustomFields
      summary: List custom fields
      description: |
        The custom fields defined in the workspace — the keys that `custom_attributes` of contacts
        and conversations can carry. The whole list comes at once.
      parameters:
        - name: applies_to
          in: query
          required: false
          description: Only the fields of contacts, of conversations or of companies.
          schema:
            type: string
            enum:
              - contact
              - conversation
              - company
      responses:
        '200':
          description: The custom fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomFieldList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /conversations:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Conversations
      operationId: listConversations
      summary: List conversations
      description: |
        The conversations you may open — the same ones your Lalexi conversation list shows you.

        Two orders:

        - **browsing** (default): most recent activity first. A conversation that gets a new message
          while you page moves to the top, so pages may shift.
        - **sync** (with `updated_since`): oldest change first, from that moment on. A conversation
          that changes while you walk the pages moves to the end instead of being skipped — keep the
          `updated_at` of the last item you saw and pass it as `updated_since` next time.
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/ConversationStatus'
        - name: channel_id
          in: query
          required: false
          description: Only conversations of this channel.
          schema:
            type: integer
        - name: contact_id
          in: query
          required: false
          description: Only conversations with this contact.
          schema:
            type: integer
        - name: phone
          in: query
          required: false
          description: Only conversations with the contact who has this phone number, in any formatting.
          schema:
            type: string
          example: '+7 900 123-45-67'
        - name: assignee_id
          in: query
          required: false
          description: Only conversations assigned to this person, or `none` for unassigned ones.
          schema:
            type: string
          example: none
        - name: team_id
          in: query
          required: false
          description: Only conversations of this team.
          schema:
            type: integer
        - name: label
          in: query
          required: false
          description: Only conversations with this label.
          schema:
            type: string
        - name: updated_since
          in: query
          required: false
          description: Only conversations changed at or after this time; switches to the sync order.
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of conversations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /conversations/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/ConversationId'
    get:
      tags:
        - Conversations
      operationId: getConversation
      summary: Get a conversation
      description: One conversation by its number. A conversation you may not open answers `404 not_found`.
      responses:
        '200':
          description: The conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags:
        - Conversations
      operationId: updateConversation
      summary: Change a conversation
      description: |
        Changes what you send and leaves the rest alone. Each change does what the same click in
        Lalexi does — the same activity line in the conversation ("resolved by …", "assigned to …")
        and the same events, with you as the one who did it. Everything is checked before anything is
        written: the request applies whole or not at all.

        An agent who reopens a conversation (`status: open`) becomes its assignee, as in Lalexi.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConversationUpdate'
            example:
              status: resolved
              labels:
                - paid
      responses:
        '200':
          description: The conversation after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: |
            `validation_failed` — an unknown status or priority, or a malformed `snoozed_until`;
            `invalid_team` — no such team in the workspace; `invalid_assignee` — that person does not
            work in this conversation's channel.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: invalid_assignee
                  message: Invalid assignee
                  details:
                    assignee_id:
                      - cannot work in this channel
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /conversations/{conversation_id}/messages:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/MessagesConversationId'
    get:
      tags:
        - Messages
      operationId: listMessages
      summary: List messages
      description: |
        The messages of a conversation in the order Lalexi shows them (by time, then by id) — newest
        first by default. Imported history is placed by its original time. Includes private notes
        and activity lines.
      parameters:
        - name: order
          in: query
          required: false
          description: '`desc` — newest first (default); `asc` — oldest first.'
          schema:
            type: string
            enum:
              - desc
              - asc
            default: desc
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Messages
      operationId: sendMessage
      summary: Send a message
      description: |
        Sends a message into the conversation as you — exactly as if you had typed it in Lalexi: on a
        messenger channel it goes out from the connected account, counts against the workspace's
        message limit, and its delivery status arrives later (`message.updated`). With `note: true`
        it is a private note for your team and is not sent to the contact.

        Files go as `multipart/form-data` in `attachments[]`: at most 10, each within the
        workspace's upload limit (40 MB unless set otherwise).

        Supports `Idempotency-Key`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreate'
            example:
              text: Your order has shipped.
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/MessageCreateWithFiles'
      responses:
        '201':
          description: The message as stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/IdempotencyInProgress'
        '422':
          description: |
            `validation_failed` — neither text nor files, or more than 10 files; `attachment_invalid` —
            a file over the size limit or one that cannot be stored; `idempotency_key_reused` — this
            key was used with a different request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: attachment_invalid
                  message: Attachment invalid
                  details:
                    attachments:
                      - each at most 40 MB
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /contacts:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Contacts
      operationId: listContacts
      summary: List and find contacts
      description: |
        Contacts of the workspace, newest first. Every member of the workspace may read them.
        Filters combine: `q` searches name, e-mail, phone and identifier; `phone` matches the
        number in any formatting; `email` matches regardless of case; `identifier` matches exactly.
      parameters:
        - name: q
          in: query
          required: false
          description: Free text over name, e-mail, phone and identifier.
          schema:
            type: string
        - name: phone
          in: query
          required: false
          description: A phone number in any formatting — `+7 (900) 123-45-67` finds `+79001234567`.
          schema:
            type: string
        - name: email
          in: query
          required: false
          schema:
            type: string
        - name: identifier
          in: query
          required: false
          schema:
            type: string
        - name: label
          in: query
          required: false
          description: Only contacts with this label.
          schema:
            type: string
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of contacts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Contacts
      operationId: createContact
      summary: Create a contact
      description: |
        Creates a contact. A contact with the same phone, e-mail or identifier already in the
        workspace answers `409 contact_exists` with its id in `details.id` — nothing is created.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactWrite'
            example:
              name: Anna Petrova
              phone: '+7 900 000-00-01'
              labels:
                - lead
              custom_attributes:
                tier: gold
      responses:
        '201':
          description: The new contact.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: '`contact_exists` — `details.id` is the existing contact.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: contact_exists
                  message: A contact with this phone, email or identifier exists.
                  details:
                    id: 1042
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /contacts/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/ContactId'
    get:
      tags:
        - Contacts
      operationId: getContact
      summary: Get a contact
      responses:
        '200':
          description: The contact.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags:
        - Contacts
      operationId: updateContact
      summary: Change a contact
      description: |
        Changes what you send and leaves the rest alone: `labels` replaces the list,
        `custom_attributes` is merged into the existing ones.

        The `identifier` of a contact that came from a messenger is that messenger's own key for
        the person and cannot be changed (`422 identifier_read_only`); sending it unchanged is fine.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactWrite'
            example:
              name: Anna Petrova
              custom_attributes:
                deal_stage: won
      responses:
        '200':
          description: The contact after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: '`validation_failed` — `details` names the fields; `identifier_read_only` — see above.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: identifier_read_only
                  message: This contact's identifier belongs to its messenger.
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /contacts/{id}/conversations:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/ContactId'
    get:
      tags:
        - Contacts
      operationId: listContactConversations
      summary: List a contact's conversations
      description: The contact's conversations that you may open, most recent activity first.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of conversations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /dialogs/probe:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Dialogs
      operationId: probeRecipient
      summary: Check a number
      description: |
        Asks the messenger of a channel whether a phone number has an account there. Nothing is
        sent. Messenger channels only; other channels answer `422 probe_not_supported`.
      parameters:
        - name: channel_id
          in: query
          required: true
          description: A messenger channel you work in.
          schema:
            type: integer
        - name: phone
          in: query
          required: true
          description: The number, in any formatting; on Telegram channels also a `@username`.
          schema:
            type: string
          example: '+79001234567'
      responses:
        '200':
          description: What the messenger said.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProbeResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: '`channel_disconnected` — the channel is not connected right now.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            `validation_failed` — no `channel_id` or `phone`; `probe_not_supported` — not a messenger
            channel; `channel_reply_only` — the channel can only answer people who wrote first;
            `invalid_recipient` — not a valid number for this messenger.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /dialogs:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags:
        - Dialogs
      operationId: startDialog
      summary: Start a dialog
      description: |
        Writes first to someone — what the **New message** button in Lalexi does — with you as the
        one who started it. Works on channels whose `capabilities.can_start_dialog` is `true`:

        - **messenger channels** — `phone` (or `contact_id`) and `text`; the message goes out from
          the connected account and the conversation appears in the workspace;
        - **e-mail** — `email` (or `contact_id`), `subject` and `text`;
        - **WhatsApp Business** — `phone` (or `contact_id`), an approved `template` of the channel,
          and `text` as the template's wording to show in the conversation.

        Outcomes:

        | HTTP | body | meaning |
        |---|---|---|
        | 201 | `status: sent` with `conversation` and `message` | sent; `message` is `null` if it has not appeared in the conversation yet, both are `null` if it joined a conversation you may not open |
        | 202 | `status: sent`, `warning: mirror_pending` | sent; the conversation will appear shortly |
        | 202 | `status: unknown` | the outcome did not come back in time — the message may have been delivered; check before sending again |
        | 409 | `duplicate` | the same text went to the same number from this channel less than 60 seconds ago |
        | 409 | `message_limit_reached` | the workspace has used its message limit (`details.limit`, `details.used`) |
        | 503 | `upstream_unavailable` | nothing was sent; retrying is safe |

        Supports `Idempotency-Key` — every outcome above, `unknown` included, is replayed for the
        same key and never sent twice. Limited to 20 starts a minute per token.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DialogStart'
            examples:
              messenger:
                summary: A messenger channel
                value:
                  channel_id: 12
                  phone: '+7 900 123-45-67'
                  text: Hello! This is Anna from the store.
              email:
                summary: An e-mail channel
                value:
                  channel_id: 15
                  email: client@example.com
                  subject: Your order
                  text: Hello! Your order is ready.
              whatsapp_business:
                summary: WhatsApp Business with a template
                value:
                  channel_id: 18
                  phone: '+79001112233'
                  text: Your package ships in 3 days.
                  template:
                    name: shipping_confirmation
                    language: en_US
                    params:
                      body:
                        '1': '3'
      responses:
        '201':
          description: Sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DialogResult'
        '202':
          description: 'Sent and not in the workspace yet (`warning: mirror_pending`), or the outcome is unknown.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DialogResult'
              examples:
                mirror_pending:
                  summary: Sent, the conversation will appear shortly
                  value:
                    status: sent
                    conversation: null
                    message: null
                    warning: mirror_pending
                unknown:
                  summary: The outcome did not come back in time
                  value:
                    status: unknown
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: |
            `duplicate` — the same text to the same recipient from this channel within 60 seconds;
            `message_limit_reached` — the workspace's message limit is used up (`details.limit`,
            `details.used`); `channel_disconnected` — the channel is not connected right now;
            `idempotency_in_progress` — a request with this key is still running.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: message_limit_reached
                  message: The workspace has used its message limit.
                  details:
                    limit: 1000
                    used: 1000
        '422':
          description: |
            `validation_failed` — `details` names the fields (no text, no recipient, a text over
            4096 characters, no subject on e-mail, no template on WhatsApp Business);
            `channel_reply_only` — the channel can only answer people who wrote first;
            `channel_cannot_start` — the channel cannot start dialogs through the API;
            `template_not_found` — not an approved template of this channel; `invalid_recipient`,
            `recipient_rejected`, `send_rejected` — the messenger refused the number or the message;
            `idempotency_key_reused` — this key was used with a different request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: channel_reply_only
                  message: This channel can only answer people who wrote first.
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Broadcasts
      operationId: listBroadcasts
      summary: List broadcasts
      description: The workspace's broadcasts — every member sees them, as in Lalexi. The whole list comes at once.
      responses:
        '200':
          description: The broadcasts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
    post:
      tags:
        - Broadcasts
      operationId: createBroadcast
      summary: Create a broadcast
      description: |
        A draft: nothing goes out until `POST /broadcasts/{id}/schedule`. Each item goes out from its
        `channel_id`, one by one with `delay_seconds` between them. Items that cannot be sent (a
        number you do not work in or that cannot write first, a bad phone, a missing text) are left
        out and listed in `errors` by their index; the rest make the broadcast. When none is left,
        nothing is created (`422 invalid_items`). Counts against the limit of 10 pieces of queued
        messenger work a minute per key (history imports, chat filters, broadcasts, rephrasing).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastCreate'
      responses:
        '201':
          description: The draft, and the items left out.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/BroadcastId'
    get:
      tags:
        - Broadcasts
      operationId: getBroadcast
      summary: Get a broadcast
      responses:
        '200':
          description: The broadcast.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
    delete:
      tags:
        - Broadcasts
      operationId: deleteBroadcast
      summary: Delete a broadcast
      description: |
        Deletes the broadcast and its items; messages already sent stay in their conversations.
        An administrator may do this to any broadcast; anyone else only when the items it touches go
        out from numbers they work in (`403 forbidden` otherwise).
      responses:
        '204':
          description: Deleted.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/{id}/schedule:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/BroadcastId'
    post:
      tags:
        - Broadcasts
      operationId: scheduleBroadcast
      summary: Start sending
      description: |
        Schedules the pending items (all of them, or `item_ids`) and starts sending.
        An administrator may do this to any broadcast; anyone else only when the items it touches go
        out from numbers they work in (`403 forbidden` otherwise).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastItemSelection'
      responses:
        '200':
          description: The broadcast as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/BroadcastId'
    post:
      tags:
        - Broadcasts
      operationId: cancelBroadcast
      summary: Cancel a broadcast
      description: |
        Stops it; items not sent yet are cancelled.
        An administrator may do this to any broadcast; anyone else only when the items it touches go
        out from numbers they work in (`403 forbidden` otherwise).
      responses:
        '200':
          description: The broadcast as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/{id}/rephrase:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/BroadcastId'
    post:
      tags:
        - Broadcasts
      operationId: rephraseBroadcast
      summary: Vary the texts with AI
      description: |
        Writes an AI variation of the text for the pending items (all, or `item_ids`) into their
        `rephrased_text`, following the workspace's prompt (`/broadcasts/rephrase-prompt`). It runs in
        the background; read the items to see the results. Counts against the limit of 10 pieces of
        queued messenger work a minute per key.
        An administrator may do this to any broadcast; anyone else only when the items it touches go
        out from numbers they work in (`403 forbidden` otherwise).
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastItemSelection'
      responses:
        '202':
          description: Queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastRephraseQueued'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/{broadcast_id}/items:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/NestedBroadcastId'
    get:
      tags:
        - Broadcasts
      operationId: listBroadcastItems
      summary: List the recipients
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of the items.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastItemList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/{broadcast_id}/items/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/NestedBroadcastId'
      - $ref: '#/components/parameters/BroadcastItemId'
    patch:
      tags:
        - Broadcasts
      operationId: updateBroadcastItem
      summary: Change one recipient
      description: |
        Changes what one recipient gets, or when; the text is saved before the time.
        An administrator may do this to any broadcast; anyone else only when the items it touches go
        out from numbers they work in (`403 forbidden` otherwise).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastItemUpdate'
      responses:
        '200':
          description: The item as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastItem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /broadcasts/rephrase-prompt:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Broadcasts
      operationId: getRephrasePrompt
      summary: AI variation prompt
      responses:
        '200':
          description: The prompt.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RephrasePrompt'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
    put:
      tags:
        - Broadcasts
      operationId: setRephrasePrompt
      summary: Set the AI variation prompt
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RephrasePrompt'
      responses:
        '200':
          description: The prompt as saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RephrasePrompt'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /usage:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Usage
      operationId: getUsage
      summary: Usage and limits
      description: |
        Messages sent over a period — paid (sent through Lalexi: replies, new dialogs, campaigns)
        and free (sent from your own phone and mirrored into Lalexi) — and where the workspace stands
        against its monthly message limit and its connection limit. The numbers of the
        **Usage** and **Limits** pages, and like them for administrators only: an agent's token
        gets `403 forbidden`. Without `from` and `to`, the last 30 days.
      parameters:
        - name: from
          in: query
          required: false
          description: Start of the period, ISO 8601.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: End of the period, ISO 8601; defaults to now.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Usage and limits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Usage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /webhooks:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags:
        - Webhooks
      operationId: listWebhooks
      summary: List your webhooks
      description: Your own webhook endpoints in this workspace (each person manages theirs). The whole list comes at once.
      responses:
        '200':
          description: Your endpoints.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Webhooks
      operationId: createWebhook
      summary: Create a webhook
      description: |
        Registers an HTTPS endpoint for the events you list. It receives only events you yourself
        may see. The answer carries the signing `secret` — the only time it is shown; store it.
        At most 10 endpoints per person per workspace.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreate'
            example:
              url: https://crm.example.com/hooks/lalexi
              events:
                - message.created
                - conversation.updated
      responses:
        '201':
          description: The endpoint, with its secret.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/WebhookId'
    get:
      tags:
        - Webhooks
      operationId: getWebhook
      summary: Get a webhook
      description: One of your endpoints, with the outcome of its last delivery. Someone else's endpoint answers `404 not_found`.
      responses:
        '200':
          description: The endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags:
        - Webhooks
      operationId: updateWebhook
      summary: Change a webhook
      description: |
        Changes the URL, the events or `active`. `active: true` turns a switched-off endpoint back on
        and clears its failure count. `rotate_secret: true` issues a new secret — the old one stops
        signing at once — and returns it in this answer only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpdate'
            example:
              active: true
      responses:
        '200':
          description: The endpoint after the change (with `secret` only if it was rotated).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags:
        - Webhooks
      operationId: deleteWebhook
      summary: Delete a webhook
      responses:
        '204':
          description: Deleted.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - $ref: '#/components/parameters/WebhookId'
    post:
      tags:
        - Webhooks
      operationId: testWebhook
      summary: Send a test delivery
      description: |
        Sends a signed `ping` event to the endpoint through the normal delivery, retries included.
        Check its outcome with `GET /webhooks/{id}` (`last_status`, `last_error`).
      responses:
        '202':
          description: The ping is on its way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTest'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
webhooks:
  message.created:
    post:
      tags:
        - Webhook events
      operationId: onMessageCreated
      summary: message.created
      description: |
        A message appeared in a conversation you may open: incoming, outgoing (sent by anyone, from
        Lalexi or from the connected phone) or a private note. Activity lines are not delivered.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  message.updated:
    post:
      tags:
        - Webhook events
      operationId: onMessageUpdated
      summary: message.updated
      description: |
        A message changed in a way you can see: its delivery `status` (delivered, read, failed and
        its `error_code`), its text, or it was deleted.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  conversation.created:
    post:
      tags:
        - Webhook events
      operationId: onConversationCreated
      summary: conversation.created
      description: A conversation you may open was created.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConversationCreatedEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  conversation.updated:
    post:
      tags:
        - Webhook events
      operationId: onConversationUpdated
      summary: conversation.updated
      description: |
        A conversation you may open changed: status, priority, assignee, team, labels, custom
        attributes or snooze time. `changed` lists what changed; one change is one delivery.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConversationUpdatedEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  contact.created:
    post:
      tags:
        - Webhook events
      operationId: onContactCreated
      summary: contact.created
      description: A contact was created in the workspace.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactCreatedEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  contact.updated:
    post:
      tags:
        - Webhook events
      operationId: onContactUpdated
      summary: contact.updated
      description: A contact changed — name, e-mail, phone, identifier, custom attributes or blocked. `changed` lists what changed.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactUpdatedEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  channel.created:
    post:
      tags:
        - Webhook events
      operationId: onChannelCreated
      summary: channel.created
      description: |
        A new number was connected — through `POST /channels` or in Lalexi. Goes to the person who
        connected it (`channel.connected_by`) and to the people who work in the channel.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelCreatedEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  channel.status_changed:
    post:
      tags:
        - Webhook events
      operationId: onChannelStatusChanged
      summary: channel.status_changed
      description: The connection status of a messenger channel you work in changed, for example from `connected` to `reconnect_required`.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelStatusChangedEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
  ping:
    post:
      tags:
        - Webhook events
      operationId: onPing
      summary: ping
      description: The test delivery sent by `POST /webhooks/{id}/test`. It cannot be subscribed to; every endpoint can receive it.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookEventHeader'
        - $ref: '#/components/parameters/WebhookDeliveryHeader'
        - $ref: '#/components/parameters/WebhookTimestampHeader'
        - $ref: '#/components/parameters/WebhookSignatureHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PingEvent'
      responses:
        '200':
          $ref: '#/components/responses/WebhookAccepted'
components:
  securitySchemes:
    personalToken:
      type: http
      scheme: bearer
      description: |
        Your personal access token from **Profile → Access Token** in Lalexi. It acts with exactly
        your role: what you may see and do in the workspace, and nothing more. Everything done with
        it is recorded as done by you.
  parameters:
    WorkspaceId:
      name: X-Workspace-Id
      in: header
      required: false
      description: |
        The workspace to work in (an id from `GET /me`). Optional when you belong to a single
        workspace; required otherwise (`400 workspace_required`).
      schema:
        type: integer
      example: 7
    Limit:
      name: limit
      in: query
      required: false
      description: Page size, 1–100 (values outside are brought into that range). Not a number → `422 validation_failed`.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    Cursor:
      name: cursor
      in: query
      required: false
      description: The `next_cursor` of the previous page. Opaque; a damaged cursor → `422 invalid_cursor`.
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Your key for this request (up to 255 characters). A repeat with the same key within 24 hours
        returns the first answer and sends nothing new; `409 idempotency_in_progress` while the first
        one runs; `422 idempotency_key_reused` for the same key with a different request.
      schema:
        type: string
        maxLength: 255
      example: crm-order-1042-confirmation
    ChannelId:
      name: id
      in: path
      required: true
      description: The channel's id.
      schema:
        type: integer
    NestedChannelId:
      name: channel_id
      in: path
      required: true
      description: The channel's id.
      schema:
        type: integer
    BroadcastId:
      name: id
      in: path
      required: true
      description: The broadcast's id.
      schema:
        type: integer
    NestedBroadcastId:
      name: broadcast_id
      in: path
      required: true
      description: The broadcast's id.
      schema:
        type: integer
    BroadcastItemId:
      name: id
      in: path
      required: true
      description: The item's id.
      schema:
        type: integer
    ConversationId:
      name: id
      in: path
      required: true
      description: The conversation's number — the one in its Lalexi URL.
      schema:
        type: integer
    MessagesConversationId:
      name: conversation_id
      in: path
      required: true
      description: The conversation's number — the one in its Lalexi URL.
      schema:
        type: integer
    ContactId:
      name: id
      in: path
      required: true
      description: The contact's id.
      schema:
        type: integer
    WebhookId:
      name: id
      in: path
      required: true
      description: The webhook endpoint's id.
      schema:
        type: integer
    WebhookEventHeader:
      name: X-Lalexi-Event
      in: header
      required: true
      description: The event name, the same as `event` in the body.
      schema:
        type: string
      example: message.created
    WebhookDeliveryHeader:
      name: X-Lalexi-Delivery
      in: header
      required: true
      description: The delivery's id, the same as `id` in the body. It stays the same across retries — deduplicate on it.
      schema:
        type: string
        format: uuid
    WebhookTimestampHeader:
      name: X-Lalexi-Timestamp
      in: header
      required: true
      description: When this attempt was signed, in Unix seconds. Part of the signed string.
      schema:
        type: string
      example: '1793093400'
    WebhookSignatureHeader:
      name: X-Lalexi-Signature
      in: header
      required: true
      description: |
        `sha256=` followed by the hex HMAC-SHA256 of `{X-Lalexi-Timestamp}.{raw body}`, keyed with
        the endpoint's whole secret (`whsec_…`). Compute it over the raw bytes you received — not over
        re-serialised JSON — and compare in constant time.
      schema:
        type: string
      example: sha256=5d41402abc4b2a76b9719d911017c592ae8f5f0c1a7b3f5e2c4d6e8f0a1b2c3d
  headers:
    RetryAfter:
      description: Seconds until the current one-minute window ends.
      schema:
        type: integer
      example: 17
  responses:
    BadRequest:
      description: '`workspace_required` — you belong to several workspaces and sent no `X-Workspace-Id`; `invalid_json` — the body is not valid JSON.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: workspace_required
              message: 'You belong to several workspaces: pass the X-Workspace-Id header.'
    Unauthorized:
      description: '`invalid_api_key` — the token is missing, unknown or not a personal token.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_api_key
              message: The API key is missing or invalid.
    Forbidden:
      description: '`forbidden` — your role does not allow this; `workspace_suspended`; `api_disabled` — API access is turned off for the workspace.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: forbidden
              message: You are not allowed to do this.
    NotFound:
      description: '`not_found` — no such object, or one you may not see (both answer the same); `workspace_not_found` — not a workspace of yours.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: not_found
              message: Resource not found.
    ValidationFailed:
      description: '`validation_failed` — `details` names the fields and what is wrong with them; `invalid_cursor` — the cursor is damaged.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: validation_failed
              message: The request is invalid.
              details:
                email:
                  - is invalid
    IdempotencyInProgress:
      description: '`idempotency_in_progress` — a request with this `Idempotency-Key` is still running.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: idempotency_in_progress
              message: A request with this Idempotency-Key is still running.
    TooManyRequests:
      description: '`rate_limited` — over a rate limit; wait `Retry-After` seconds.'
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: Too many requests. Retry after the number of seconds in Retry-After.
    InternalError:
      description: '`internal_error` — something went wrong on our side.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: internal_error
              message: Something went wrong on our side.
    UpstreamUnavailable:
      description: '`upstream_unavailable` — the messaging service did not answer. Nothing was sent; retrying is safe.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: upstream_unavailable
              message: The messaging service is unavailable. Nothing was sent; try again.
    WebhookAccepted:
      description: Any 2xx status within 5 seconds acknowledges the delivery (the whole delivery is cut off at 15 seconds); the response body is ignored.
  schemas:
    Error:
      type: object
      description: The body of every error answer.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: For people, in English. May change — branch on `code`.
            details:
              type: object
              additionalProperties: true
              description: |
                More about the error, when there is more: for `validation_failed` the failed fields,
                each with a list of problems; for `contact_exists` the existing contact's `id`; for
                `message_limit_reached` the `limit` and `used` numbers.
    ErrorCode:
      type: string
      description: |
        The closed list of error codes. New codes may be added within `v1` (see Compatibility):
        treat an unknown code by its HTTP status.

        | code | HTTP | meaning |
        |---|---|---|
        | `invalid_json` | 400 | The body is not valid JSON. |
        | `workspace_required` | 400 | Several workspaces and no `X-Workspace-Id`. |
        | `invalid_api_key` | 401 | The token is missing, unknown or not a personal token. |
        | `forbidden` | 403 | Your role does not allow this. |
        | `channel_unavailable` | 403 | This channel type is not open to the workspace yet. |
        | `workspace_suspended` | 403 | The workspace is suspended. |
        | `api_disabled` | 403 | API access is turned off for the workspace. |
        | `not_found` | 404 | No such object, or not one you may see; also any unknown path. |
        | `workspace_not_found` | 404 | Not a workspace of yours. |
        | `contact_exists` | 409 | A contact with this phone, e-mail or identifier exists (`details.id`). |
        | `idempotency_in_progress` | 409 | A request with this `Idempotency-Key` is still running. |
        | `duplicate` | 409 | The same dialog start (channel, recipient, text) within 60 seconds. |
        | `message_limit_reached` | 409 | The workspace's message limit is used up (`details.limit`, `details.used`). |
        | `channel_disconnected` | 409 | The channel is not connected right now. |
        | `connection_limit_reached` | 409 | The plan's connections are all in use (`details.limit`, `details.used`). |
        | `validation_failed` | 422 | The request is invalid; `details` names the fields. |
        | `invalid_cursor` | 422 | The cursor is damaged. |
        | `idempotency_key_reused` | 422 | This `Idempotency-Key` was used with a different request. |
        | `identifier_read_only` | 422 | A messenger contact's identifier cannot be changed. |
        | `invalid_assignee` | 422 | That person does not work in the conversation's channel. |
        | `invalid_team` | 422 | No such team in the workspace. |
        | `attachment_invalid` | 422 | A file is too large or cannot be stored. |
        | `channel_reply_only` | 422 | The channel can only answer people who wrote first. |
        | `channel_cannot_start` | 422 | The channel cannot start dialogs through the API. |
        | `probe_not_supported` | 422 | Number checks work on messenger channels only. |
        | `template_not_found` | 422 | Not an approved WhatsApp Business template of this channel. |
        | `invalid_recipient` | 422 | Not a valid recipient for the channel. |
        | `recipient_rejected` | 422 | The messenger refused this recipient. |
        | `send_rejected` | 422 | The messenger refused the message. |
        | `not_supported` | 422 | The channel does not support this (a native channel, a bot without a chat list). |
        | `channel_connected` | 422 | Disconnect the channel before hiding it. |
        | `invalid_items` | 422 | No item of the broadcast can be sent; `details.errors` says why for each. |
        | `rate_limited` | 429 | Over a rate limit; see `Retry-After`. |
        | `internal_error` | 500 | Something went wrong on our side. |
        | `upstream_unavailable` | 503 | The messaging service did not answer; nothing was sent. |
      enum:
        - invalid_json
        - workspace_required
        - invalid_api_key
        - forbidden
        - channel_unavailable
        - workspace_suspended
        - api_disabled
        - not_found
        - workspace_not_found
        - contact_exists
        - idempotency_in_progress
        - duplicate
        - message_limit_reached
        - channel_disconnected
        - connection_limit_reached
        - validation_failed
        - invalid_cursor
        - idempotency_key_reused
        - identifier_read_only
        - invalid_assignee
        - invalid_team
        - attachment_invalid
        - channel_reply_only
        - channel_cannot_start
        - probe_not_supported
        - template_not_found
        - invalid_recipient
        - recipient_rejected
        - send_rejected
        - not_supported
        - channel_connected
        - invalid_items
        - rate_limited
        - internal_error
        - upstream_unavailable
    NextCursor:
      type:
        - string
        - 'null'
      description: Pass it as `cursor` for the next page; `null` on the last page.
    Person:
      type: object
      description: A member of the workspace, as shown in Lalexi.
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string
    Workspace:
      type: object
      required:
        - id
        - name
        - locale
        - role
        - custom_role
      properties:
        id:
          type: integer
          description: The value for `X-Workspace-Id`.
        name:
          type: string
        locale:
          type: string
          description: The workspace's language, for example `ru` or `en`.
        role:
          $ref: '#/components/schemas/Role'
        custom_role:
          type:
            - string
            - 'null'
          description: The name of your custom role, if you have one.
    Me:
      type: object
      required:
        - user
        - workspaces
      properties:
        user:
          type: object
          required:
            - id
            - name
            - email
          properties:
            id:
              type: integer
            name:
              type: string
            email:
              type: string
              format: email
        workspaces:
          type: array
          items:
            $ref: '#/components/schemas/Workspace'
    Role:
      type: string
      enum:
        - administrator
        - agent
    ChannelType:
      type: string
      description: |
        What the channel is. An open list — new types may appear; treat an unknown one as `other`.

        Messenger channels connected to Lalexi: `whatsapp` (WhatsApp Personal), `whatsapp2`
        (WhatsApp Personal, alternative connection), `telegram` (Telegram Personal), `telegram2`
        (Telegram Personal, alternative connection), `max` (MAX Personal), `maxbot` (MAX bot),
        `zalo` (Zalo bot), `viber` (Viber bot), `viberbm` (Viber Business), `avito`, `cian`.

        Other channels: `whatsapp_business` (official WhatsApp Business), `telegram_bot`, `email`,
        `website` (the website chat), `facebook`, `instagram`, `sms`, `line`, `tiktok`, `twitter`,
        `api` (your own API channel), `other`.
      examples:
        - whatsapp
    ChannelStatus:
      type: string
      description: |
        The connection status. `connected`; `disconnected` — the account was disconnected;
        `reconnect_required` — the account needs to be connected again (scan the code or sign in);
        `rate_limited` — the messenger is throttling the account for now: a warning, it is still
        connected; `error` — any other problem.
      enum:
        - connected
        - disconnected
        - reconnect_required
        - rate_limited
        - error
    ChannelCapabilities:
      type: object
      required:
        - can_start_dialog
        - reply_only
        - attachments
        - chat_list
        - broadcasts
      properties:
        can_start_dialog:
          type: boolean
          description: '`POST /dialogs` works on this channel.'
        reply_only:
          type: boolean
          description: The channel can only answer people who wrote first (bots, marketplaces).
        attachments:
          type: boolean
          description: Files can be sent.
        chat_list:
          type: boolean
          description: The number has a chat list and history (`/chats`, `/chat-filter`, `/history-import`); bots do not.
        broadcasts:
          type: boolean
          description: The number can send broadcasts.
    Channel:
      type: object
      description: A channel of the workspace — one messenger account or bot, an e-mail box, the website chat.
      required:
        - id
        - name
        - type
        - status
        - connected_by
        - note
        - hidden
        - capabilities
        - created_at
      properties:
        id:
          type: integer
        name:
          type: string
        type:
          $ref: '#/components/schemas/ChannelType'
        status:
          $ref: '#/components/schemas/ChannelStatus'
        connected_by:
          description: Who connected the channel; `null` when unknown or no longer in the workspace.
          anyOf:
            - $ref: '#/components/schemas/Person'
            - type: 'null'
        note:
          description: Your team's label on a connected number (`PATCH /channels/{id}`).
          type:
            - string
            - 'null'
        hidden:
          type: boolean
          description: A disconnected number taken out of the workspace's sight, with its conversations.
        capabilities:
          $ref: '#/components/schemas/ChannelCapabilities'
        created_at:
          type: string
          format: date-time
    ChannelList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Channel'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ExternalId:
      type: string
      maxLength: 255
      description: Your own id for the message, returned as `external_id` on it and in its webhooks.
    ConnectableType:
      type: string
      description: A messenger number or bot `POST /channels` can connect.
      enum:
        - whatsapp
        - whatsapp2
        - telegram
        - telegram2
        - max
        - maxbot
        - zalo
        - viber
        - avito
        - cian
        - viberbm
    ChannelConnect:
      type: object
      required:
        - type
      properties:
        type:
          $ref: '#/components/schemas/ConnectableType'
    ConnectLink:
      type: object
      required:
        - type
        - connect_url
      properties:
        type:
          $ref: '#/components/schemas/ChannelType'
        connect_url:
          type: string
          format: uri
          description: The page to open. Short-lived — start a new one for every attempt.
    ChannelUpdate:
      type: object
      properties:
        note:
          type:
            - string
            - 'null'
          maxLength: 255
        hidden:
          type: boolean
    ChannelTypeInfo:
      type: object
      required:
        - type
        - name
        - available
        - capabilities
      properties:
        type:
          $ref: '#/components/schemas/ConnectableType'
        name:
          type: string
          examples:
            - WhatsApp Personal
        available:
          type: boolean
          description: The workspace may connect this type now.
        capabilities:
          type: object
          required:
            - can_start_dialog
            - reply_only
            - chat_list
            - broadcasts
          properties:
            can_start_dialog:
              type: boolean
            reply_only:
              type: boolean
            chat_list:
              type: boolean
            broadcasts:
              type: boolean
    ChannelTypeList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ChannelTypeInfo'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    HistoryImportStart:
      type: object
      properties:
        deep:
          type: boolean
          default: true
          description: Everything the messenger keeps (`true`) or the recent messages of each chat (`false`).
    HistoryImportQueued:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          const: queued
    HistoryImportProgress:
      type: object
      required:
        - status
        - chats_imported
        - chats_planned
        - current_chat
        - messages_imported
      properties:
        status:
          type: string
          enum:
            - idle
            - running
            - done
            - failed
        chats_imported:
          type: integer
        chats_planned:
          description: Known when the filter names its chats.
          type:
            - integer
            - 'null'
        current_chat:
          anyOf:
            - type: object
              required:
                - name
                - messages_imported
                - messages_total
              properties:
                name:
                  type: string
                messages_imported:
                  type: integer
                messages_total:
                  type:
                    - integer
                    - 'null'
            - type: 'null'
        messages_imported:
          type: integer
    AccountChat:
      type: object
      required:
        - id
        - name
        - phone
        - last_activity_at
      properties:
        id:
          type: string
          description: The chat's id in the messenger — what `chat_ids` of a filter take.
        name:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        last_activity_at:
          type:
            - string
            - 'null'
          format: date-time
    AccountChatList:
      type: object
      required:
        - status
        - data
      properties:
        status:
          type: string
          description: '`loading` — still fetching, ask again shortly; `ready`; `failed`; `unavailable` — the number has no chat list.'
          enum:
            - loading
            - ready
            - failed
            - unavailable
        data:
          type: array
          items:
            $ref: '#/components/schemas/AccountChat'
    ChatFilter:
      type: object
      required:
        - mode
      properties:
        mode:
          type: string
          enum:
            - all
            - include
            - exclude
        chat_ids:
          type: array
          maxItems: 5000
          description: Required for `include` and `exclude`.
          items:
            type: string
            maxLength: 128
    ChatFilterAllow:
      type: object
      properties:
        phone:
          type: string
          maxLength: 64
        chat_id:
          type: string
          maxLength: 128
    ChatFilterAllowed:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          const: allowed
    BroadcastStatus:
      type: string
      description: '`draft` — created, nothing sent until scheduled; `scheduled`; `in_progress`; `completed`; `failed`; `cancelled`.'
      enum:
        - draft
        - scheduled
        - in_progress
        - completed
        - failed
        - cancelled
    BroadcastItemStatus:
      type: string
      description: |
        `pending` — not scheduled yet; `scheduled`; `paused` — its number dropped, it resumes on
        reconnect; `sending`; `sent` — accepted by the messenger; `delivered`; `read`;
        `undelivered`; `failed`; `cancelled`.
      enum:
        - pending
        - scheduled
        - paused
        - sending
        - sent
        - delivered
        - read
        - undelivered
        - failed
        - cancelled
    Broadcast:
      type: object
      required:
        - id
        - name
        - status
        - items_total
        - items_by_status
        - created_at
        - scheduled_at
        - completed_at
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          $ref: '#/components/schemas/BroadcastStatus'
        items_total:
          type: integer
        items_by_status:
          type: object
          description: How many items are in each `BroadcastItemStatus`; a status with none is left out.
          additionalProperties:
            type: integer
        created_at:
          type: string
          format: date-time
        scheduled_at:
          type:
            - string
            - 'null'
          format: date-time
        completed_at:
          type:
            - string
            - 'null'
          format: date-time
    BroadcastList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Broadcast'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    BroadcastItemInput:
      type: object
      required:
        - channel_id
        - text
      properties:
        channel_id:
          type: integer
          description: The number it goes out from — one you work in whose `capabilities.broadcasts` is `true`.
        phone:
          type: string
          description: The recipient, 10–15 digits in any formatting. Required unless `chat_id` is given.
        chat_id:
          type: string
          maxLength: 128
          description: An existing chat of that number instead of `phone` (an id from `GET /channels/{channel_id}/chats`).
        name:
          type: string
        text:
          type: string
          maxLength: 4096
        delay_seconds:
          type: integer
          minimum: 30
          maximum: 300
          description: The pause before this item; the broadcast's `default_delay_seconds` when left out.
        scheduled_at:
          type: string
          format: date-time
          description: Not before this time.
    BroadcastCreate:
      type: object
      required:
        - name
        - items
      properties:
        name:
          type: string
          maxLength: 255
        default_delay_seconds:
          type: integer
          minimum: 30
          maximum: 300
          default: 30
        items:
          type: array
          minItems: 1
          maxItems: 10000
          items:
            $ref: '#/components/schemas/BroadcastItemInput'
    ItemError:
      type: object
      required:
        - index
        - message
      properties:
        index:
          description: The position of the item in `items`, from 0; `null` for the list as a whole.
          type:
            - integer
            - 'null'
        message:
          type: string
    BroadcastCreated:
      type: object
      required:
        - broadcast
        - errors
      properties:
        broadcast:
          $ref: '#/components/schemas/Broadcast'
        errors:
          type: array
          description: The items left out, and why.
          items:
            $ref: '#/components/schemas/ItemError'
    BroadcastItem:
      type: object
      required:
        - id
        - channel_id
        - phone
        - chat_id
        - name
        - text
        - rephrased_text
        - status
        - error_code
        - delay_seconds
        - scheduled_at
        - sent_at
      properties:
        id:
          type: integer
        channel_id:
          description: The number it goes out from; `null` if that number left the workspace, or while an item sent from an account group waits for an account.
          type:
            - integer
            - 'null'
        phone:
          type:
            - string
            - 'null'
        chat_id:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        text:
          type: string
        rephrased_text:
          description: The text it goes out with instead of `text` (an AI variation or your edit).
          type:
            - string
            - 'null'
        status:
          $ref: '#/components/schemas/BroadcastItemStatus'
        error_code:
          description: Set when `status` is `failed` or `undelivered`.
          anyOf:
            - $ref: '#/components/schemas/MessageErrorCode'
            - type: 'null'
        delay_seconds:
          type: integer
        scheduled_at:
          type:
            - string
            - 'null'
          format: date-time
        sent_at:
          type:
            - string
            - 'null'
          format: date-time
    BroadcastItemList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/BroadcastItem'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    BroadcastItemUpdate:
      type: object
      properties:
        text:
          type: string
          maxLength: 4096
          description: What this recipient gets instead; empty goes back to the broadcast's text.
        scheduled_at:
          type: string
          format: date-time
          description: A new time for an item not sent yet.
    BroadcastItemSelection:
      type: object
      properties:
        item_ids:
          type: array
          description: Only these items; all pending ones when left out.
          items:
            type: integer
    BroadcastRephraseQueued:
      type: object
      required:
        - queued
      properties:
        queued:
          type: integer
          description: How many items will get an AI variation of their text.
    RephrasePrompt:
      type: object
      required:
        - prompt
      properties:
        prompt:
          description: The workspace's instruction for AI variations; `null` — the default one.
          type:
            - string
            - 'null'
          maxLength: 2000
    Agent:
      type: object
      description: A person in the workspace.
      required:
        - id
        - name
        - email
        - role
        - custom_role
        - availability
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        role:
          $ref: '#/components/schemas/Role'
        custom_role:
          type:
            - string
            - 'null'
          description: The name of the person's custom role, if any.
        availability:
          type: string
          enum:
            - online
            - busy
            - offline
    AgentList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    Team:
      type: object
      required:
        - id
        - name
        - description
        - allow_auto_assign
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        allow_auto_assign:
          type: boolean
          description: New conversations of the team are assigned to its members automatically.
    TeamList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Team'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    Label:
      type: object
      required:
        - id
        - title
        - description
        - color
      properties:
        id:
          type: integer
        title:
          type: string
          description: What you put in `labels` of a conversation or a contact.
        description:
          type:
            - string
            - 'null'
        color:
          type: string
          description: A hex color.
          examples:
            - '#1f93ff'
    LabelList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Label'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    CustomField:
      type: object
      description: A custom field — a key that `custom_attributes` of contacts or conversations can carry.
      required:
        - id
        - key
        - name
        - description
        - type
        - applies_to
        - values
      properties:
        id:
          type: integer
        key:
          type: string
          description: The key in `custom_attributes`.
        name:
          type: string
          description: The name shown in Lalexi.
        description:
          type:
            - string
            - 'null'
        type:
          type: string
          enum:
            - text
            - number
            - currency
            - percent
            - link
            - date
            - list
            - checkbox
        applies_to:
          type: string
          enum:
            - contact
            - conversation
            - company
        values:
          type: array
          description: The options of a `list` field; empty otherwise.
          items:
            type: string
    CustomFieldList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/CustomField'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ConversationStatus:
      type: string
      enum:
        - open
        - pending
        - snoozed
        - resolved
    ConversationPriority:
      type: string
      enum:
        - low
        - medium
        - high
        - urgent
    Conversation:
      type: object
      required:
        - id
        - status
        - priority
        - channel
        - contact
        - assignee
        - team
        - labels
        - custom_attributes
        - snoozed_until
        - created_at
        - updated_at
        - last_activity_at
      properties:
        id:
          type: integer
          description: The conversation's number — the one in its Lalexi URL.
        status:
          $ref: '#/components/schemas/ConversationStatus'
        priority:
          anyOf:
            - $ref: '#/components/schemas/ConversationPriority'
            - type: 'null'
        channel:
          type: object
          required:
            - id
            - name
            - type
          properties:
            id:
              type: integer
            name:
              type: string
            type:
              $ref: '#/components/schemas/ChannelType'
        contact:
          anyOf:
            - $ref: '#/components/schemas/ContactSummary'
            - type: 'null'
        assignee:
          anyOf:
            - $ref: '#/components/schemas/Person'
            - type: 'null'
        team:
          anyOf:
            - $ref: '#/components/schemas/TeamSummary'
            - type: 'null'
        labels:
          type: array
          items:
            type: string
        custom_attributes:
          type: object
          additionalProperties: true
          description: Values of the conversation's custom fields, by key.
        snoozed_until:
          type:
            - string
            - 'null'
          format: date-time
          description: When a snoozed conversation reopens; `null` if it is not snoozed or sleeps until the next message.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_activity_at:
          type: string
          format: date-time
    ConversationList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Conversation'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ContactSummary:
      type: object
      required:
        - id
        - name
        - phone
        - email
      properties:
        id:
          type: integer
        name:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
    TeamSummary:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string
    ConversationUpdate:
      type: object
      description: Send only what you change.
      properties:
        status:
          $ref: '#/components/schemas/ConversationStatus'
        snoozed_until:
          type: string
          format: date-time
          description: With `status` `snoozed` — when the conversation reopens. Without it, it sleeps until the next message.
        priority:
          description: '`null` clears the priority.'
          anyOf:
            - $ref: '#/components/schemas/ConversationPriority'
            - type: 'null'
        assignee_id:
          type:
            - integer
            - 'null'
          description: A person who works in the conversation's channel (its members and the administrators); `null` unassigns.
        team_id:
          type:
            - integer
            - 'null'
          description: A team of the workspace; `null` removes the team.
        labels:
          type: array
          description: Replaces the conversation's labels.
          items:
            type: string
        custom_attributes:
          type: object
          additionalProperties: true
          description: Merged into the existing values; keys you do not send are kept.
    Sender:
      type: object
      description: |
        Who wrote the message: `agent` — a person of the workspace; `contact` — the person you talk
        to; `bot` — an assistant or bot; `system` — Lalexi itself (campaigns, warm-up, automatic
        messages), with `id` and `name` `null`.
      required:
        - type
        - id
        - name
      properties:
        type:
          type: string
          enum:
            - agent
            - contact
            - bot
            - system
        id:
          type:
            - integer
            - 'null'
        name:
          type:
            - string
            - 'null'
    Attachment:
      type: object
      required:
        - id
        - type
        - url
        - file_name
        - size
        - content_type
      properties:
        id:
          type: integer
        type:
          type: string
          description: An open list — `image`, `audio`, `video`, `file`, `location`, `contact`, `share`, `fallback` and others.
          examples:
            - image
        url:
          type:
            - string
            - 'null'
          description: Where to download the file.
        file_name:
          type:
            - string
            - 'null'
        size:
          type:
            - integer
            - 'null'
          description: Bytes.
        content_type:
          type:
            - string
            - 'null'
          examples:
            - image/png
    MessageErrorCode:
      type: string
      description: |
        Why a message failed: `message_limit_reached` — the workspace's message limit was used up;
        `channel_disconnected` — the channel was not connected; `rate_limited` — the messenger was
        throttling the account; `send_failed` — any other reason.
      enum:
        - message_limit_reached
        - channel_disconnected
        - rate_limited
        - send_failed
    Message:
      type: object
      required:
        - id
        - conversation_id
        - type
        - text
        - deleted
        - sender
        - status
        - error_code
        - attachments
        - via
        - external_id
        - created_at
        - updated_at
      properties:
        id:
          type: integer
        conversation_id:
          type: integer
          description: The conversation's number.
        type:
          type: string
          description: '`incoming` — from the contact; `outgoing` — to the contact; `note` — a private note of the team; `activity` — a line like "resolved by …".'
          enum:
            - incoming
            - outgoing
            - note
            - activity
        text:
          type:
            - string
            - 'null'
          description: '`null` for a deleted message or a message with files only.'
        deleted:
          type: boolean
        sender:
          $ref: '#/components/schemas/Sender'
        status:
          type: string
          description: Delivery status of an outgoing message.
          enum:
            - sent
            - delivered
            - read
            - failed
        error_code:
          description: Set when `status` is `failed`.
          anyOf:
            - $ref: '#/components/schemas/MessageErrorCode'
            - type: 'null'
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/Attachment'
        via:
          description: |
            `api` — written through this API (`POST /conversations/{conversation_id}/messages`,
            `POST /dialogs`), by anyone's key; `null` — written in Lalexi, on the phone, or by the
            contact. Tells an integration its own messages apart in webhooks.
          anyOf:
            - type: string
              enum:
                - api
            - type: 'null'
        external_id:
          description: The `external_id` the API caller gave the message; `null` when none.
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    MessageList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Message'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    MessageCreate:
      type: object
      properties:
        text:
          type: string
          description: The message. Required unless files are attached.
        external_id:
          $ref: '#/components/schemas/ExternalId'
        note:
          type: boolean
          default: false
          description: '`true` writes a private note for your team; it is not sent to the contact.'
    MessageCreateWithFiles:
      type: object
      properties:
        text:
          type: string
          description: The message or the caption. Optional with files.
        note:
          type: boolean
          default: false
        'attachments[]':
          type: array
          maxItems: 10
          description: Up to 10 files, each within the workspace's upload limit (40 MB unless set otherwise).
          items:
            type: string
            contentMediaType: application/octet-stream
    Contact:
      type: object
      required:
        - id
        - name
        - phone
        - email
        - identifier
        - social_profiles
        - labels
        - custom_attributes
        - blocked
        - created_at
        - updated_at
        - last_activity_at
      properties:
        id:
          type: integer
        name:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
          description: E.164, for example `+79001234567`.
        email:
          type:
            - string
            - 'null'
        identifier:
          type:
            - string
            - 'null'
          description: Your key for the person, or — for a contact that came from a messenger — that messenger's key.
        social_profiles:
          type: object
          additionalProperties:
            type: string
          description: Profiles in social networks and messengers, by network.
        labels:
          type: array
          items:
            type: string
        custom_attributes:
          type: object
          additionalProperties: true
          description: Values of the contact's custom fields, by key.
        blocked:
          type: boolean
          description: Whether the contact is blocked in Lalexi.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_activity_at:
          type:
            - string
            - 'null'
          format: date-time
    ContactList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Contact'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ContactWrite:
      type: object
      description: Send only what you set or change.
      properties:
        name:
          type: string
        phone:
          type: string
          description: In any formatting; stored as E.164 (`+7 900 000-00-01` → `+79000000001`).
        email:
          type: string
          format: email
        identifier:
          type: string
          description: Your own key for the person, such as the id in your CRM. Read-only on a contact that came from a messenger.
        blocked:
          type: boolean
        labels:
          type: array
          description: Replaces the contact's labels.
          items:
            type: string
        custom_attributes:
          type: object
          additionalProperties: true
          description: Merged into the existing values; keys you do not send are kept.
    DialogTemplate:
      type: object
      description: An approved WhatsApp Business template of the channel.
      required:
        - name
        - language
      properties:
        name:
          type: string
        language:
          type: string
          examples:
            - en_US
        params:
          type: object
          additionalProperties: true
          description: 'Values for the template''s placeholders, by component, for example `{"body": {"1": "3"}}`.'
    DialogStart:
      type: object
      required:
        - channel_id
        - text
      properties:
        channel_id:
          type: integer
          description: A channel you work in whose `capabilities.can_start_dialog` is `true`.
        phone:
          type: string
          maxLength: 64
          description: |
            The recipient on messenger channels and WhatsApp Business — a phone number in any
            formatting (10–15 digits); on Telegram channels also a `@username`.
        email:
          type: string
          format: email
          description: The recipient on an e-mail channel.
        contact_id:
          type: integer
          description: Instead of `phone` / `email` — write to this contact (at its phone, or its e-mail on an e-mail channel).
        text:
          type: string
          maxLength: 4096
        subject:
          type: string
          description: The subject line. Required on an e-mail channel.
        template:
          $ref: '#/components/schemas/DialogTemplate'
        external_id:
          $ref: '#/components/schemas/ExternalId'
    DialogResult:
      type: object
      description: |
        `status: sent` — the message went out: `conversation` and `message` are the started
        conversation and its first message (each `null` when it cannot be shown yet or to you), and
        `warning: mirror_pending` means the conversation will appear shortly. `status: unknown` —
        the outcome did not come back in time; the other fields are absent.
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - sent
            - unknown
        conversation:
          anyOf:
            - $ref: '#/components/schemas/Conversation'
            - type: 'null'
        message:
          anyOf:
            - $ref: '#/components/schemas/Message'
            - type: 'null'
        warning:
          type: string
          enum:
            - mirror_pending
    ProbeResult:
      type: object
      required:
        - status
        - phone
        - name
      properties:
        status:
          type: string
          description: '`found` — the number has an account on this messenger; `not_found` — it has none; `unknown` — the messenger did not say.'
          enum:
            - found
            - not_found
            - unknown
        phone:
          type:
            - string
            - 'null'
          description: The number as the messenger understood it.
        name:
          type:
            - string
            - 'null'
          description: The account's name, when the messenger shares it.
    Usage:
      type: object
      required:
        - period
        - messages
        - connections
      properties:
        period:
          type: object
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
        messages:
          type: object
          required:
            - paid
            - free
            - limit
            - used_this_period
            - period_start
            - limit_reached
          properties:
            paid:
              type: integer
              description: Messages sent through Lalexi in the period (replies, new dialogs, campaigns).
            free:
              type: integer
              description: Messages sent from your own phones in the period and mirrored into Lalexi.
            limit:
              type:
                - integer
                - 'null'
              description: The monthly limit of paid messages; `null` — unlimited.
            used_this_period:
              type: integer
              description: Paid messages since `period_start`.
            period_start:
              type:
                - string
                - 'null'
              format: date-time
              description: The start of the current limit month (UTC).
            limit_reached:
              type: boolean
        connections:
          type: object
          required:
            - limit
            - used
            - limit_reached
          properties:
            limit:
              type:
                - integer
                - 'null'
              description: How many channels may be connected; `null` — unlimited.
            used:
              type: integer
            limit_reached:
              type: boolean
    WebhookEvent:
      type: string
      description: An event an endpoint can subscribe to.
      enum:
        - message.created
        - message.updated
        - conversation.created
        - conversation.updated
        - contact.created
        - contact.updated
        - channel.created
        - channel.status_changed
    Webhook:
      type: object
      description: One of your webhook endpoints.
      required:
        - id
        - url
        - events
        - active
        - consecutive_failures
        - last_delivery_at
        - last_status
        - last_error
        - created_at
      properties:
        id:
          type: integer
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEvent'
        active:
          type: boolean
          description: '`false` — switched off, by you or after 50 failed attempts in a row.'
        consecutive_failures:
          type: integer
          description: Failed attempts in a row; a success resets it.
        last_delivery_at:
          type:
            - string
            - 'null'
          format: date-time
        last_status:
          type:
            - integer
            - 'null'
          description: The HTTP status of the last attempt; `null` when none came back.
        last_error:
          type:
            - string
            - 'null'
          description: Why the last attempt failed; `null` after a success.
        created_at:
          type: string
          format: date-time
        secret:
          type: string
          description: The signing secret (`whsec_…`). Present only in the answers of create and of `rotate_secret`.
    WebhookList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Webhook'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    WebhookCreate:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: An `https://` URL on the public internet. Private and local addresses are refused at delivery.
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEvent'
        active:
          type: boolean
          default: true
    WebhookUpdate:
      type: object
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEvent'
        active:
          type: boolean
          description: '`true` turns the endpoint back on and clears `consecutive_failures`.'
        rotate_secret:
          type: boolean
          description: '`true` issues a new secret and returns it in this answer.'
    WebhookTest:
      type: object
      required:
        - delivery_id
      properties:
        delivery_id:
          type: string
          format: uuid
          description: The `X-Lalexi-Delivery` of the ping.
    MessageEvent:
      type: object
      description: The body of `message.created` and `message.updated`.
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
          description: The delivery's id (`X-Lalexi-Delivery`).
        event:
          type: string
          enum:
            - message.created
            - message.updated
        created_at:
          type: string
          format: date-time
          description: When the event happened.
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - message
            - conversation
          properties:
            message:
              $ref: '#/components/schemas/Message'
            conversation:
              $ref: '#/components/schemas/Conversation'
    ConversationCreatedEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: conversation.created
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - conversation
          properties:
            conversation:
              $ref: '#/components/schemas/Conversation'
    ConversationUpdatedEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: conversation.updated
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - conversation
            - changed
          properties:
            conversation:
              $ref: '#/components/schemas/Conversation'
            changed:
              type: array
              description: The fields of `conversation` that changed.
              items:
                type: string
                enum:
                  - status
                  - priority
                  - assignee
                  - team
                  - labels
                  - custom_attributes
                  - snoozed_until
    ContactCreatedEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: contact.created
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - contact
          properties:
            contact:
              $ref: '#/components/schemas/Contact'
    ContactUpdatedEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: contact.updated
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - contact
            - changed
          properties:
            contact:
              $ref: '#/components/schemas/Contact'
            changed:
              type: array
              description: The fields of `contact` that changed.
              items:
                type: string
                enum:
                  - name
                  - email
                  - phone
                  - identifier
                  - custom_attributes
                  - blocked
    ChannelStatusChangedEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: channel.status_changed
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - channel
            - previous_status
            - status
          properties:
            channel:
              $ref: '#/components/schemas/Channel'
            previous_status:
              $ref: '#/components/schemas/ChannelStatus'
            status:
              $ref: '#/components/schemas/ChannelStatus'
    ChannelCreatedEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: channel.created
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - channel
          properties:
            channel:
              $ref: '#/components/schemas/Channel'
    PingEvent:
      type: object
      required:
        - id
        - event
        - created_at
        - workspace_id
        - data
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          const: ping
        created_at:
          type: string
          format: date-time
        workspace_id:
          type: integer
        data:
          type: object
          required:
            - message
          properties:
            message:
              type: string
              examples:
                - Webhook test from your workspace
