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

# Initiate an outbound escalation (harness-started SMS thread)

> Open a BRAND-NEW SMS conversation with a customer and send its first message, with the thread owned by the calling connector from the first instant. This is the outbound-initiated sibling of the inbound escalation path: once the session exists the customer's replies route to your harness automatically (the AI does not answer them), so you continue on POST /escalations/{id}/messages exactly as you would on any escalation you claimed. The session is created ALREADY CLAIMED by the calling key and active — you do not race a claim SLA on a thread you started. COMPLIANCE (all enforced server-side, none of it optional):
* There is no `from` field. The sending number is derived from the agent's SMS-active number; an agent with none returns 422 `no_sms_number`.
* `to` must be a US or Canada (+1) number. The platform's numbers and its A2P 10DLC/TCR registration are North American, so any other destination is refused 422 `unsupported_country` — nothing is sent.
* Volume is capped per workspace: a rolling 24-hour ceiling on how many conversations a harness may START (200 by default). Past it, 429 `daily_initiate_cap` carries `details.nextOpen` and `Retry-After`. An idempotent replay is answered from the original receipt and is never capped.
* The recipient must have a recorded consent basis — `conversational` (a customer who texted you first) or `express` (captured in a call or chat). Otherwise 422 `consent_required`. Orgs that hold consent out-of-band can be granted a bypass, which is recorded in the compliance trail.
* The first text is business-initiated, so it is quiet-hours gated and carries the required brand + "Msg & data rates may apply. Reply HELP for help, STOP to opt out." disclosure block, appended by the platform. Do not add your own.
* An initiate that did NOT send is always an explicit error, never a 200 with delivered:false. A quiet-hours refusal is 409 `quiet_hours` and carries `details.nextOpen` — retry after it. IDEMPOTENCY. The key is (organization, `client_message_id`) — durable and stable, so a replay returns the original receipt (byte for byte) and never sends a second text: after the original escalation was resolved, after the 72-hour conversation-episode rollover, and against a concurrent duplicate of itself. Reusing an id for a DIFFERENT message is 409 `client_message_id_reused`. WHEN THE OUTCOME IS UNKNOWN. If the carrier request fails in a way that does not prove the message stayed home (a timeout, a transport failure, a 5xx), the answer is 409 `send_state_unknown` carrying `details.escalationId` and `details.conversationId`. It is the ONLY refusal here that does not promise the recipient's phone stayed silent, so do not blindly retry: read the thread, or replay the SAME `client_message_id` — a replay resolves to the same answer, or to the receipt once the state settles, and never sends a second text. Requires the deployment to have the Agent Bridge enabled (503 `bridge_disabled`), the org to be enabled for harness-initiated conversations (403 `harness_outbound_disabled`) and on a Starter plan or higher (403 `plan_required`).




## OpenAPI

````yaml /openapi.yaml post /escalations
openapi: 3.1.0
info:
  title: Flowyte V2 Control-Plane REST API
  version: 1.0.0
  description: >-
    The single REST API for the Flowyte platform (base path `/api/v1`).
    Authenticate every request with a secret API key — `Authorization: Bearer
    flowyte_sk_…` — and each operation lists the scope the key must hold.
    Successful responses use the `ApiResponse<T>` envelope; list responses use
    cursor-based `PaginatedResponse<T>`. Errors are RFC 9457 problem+json.
    Streaming endpoints return Server-Sent Events
    (`event:<type>\ndata:<json>\n\n`, terminating with `event: done`).
  contact:
    name: Flowyte Platform
  license:
    name: Proprietary
servers:
  - url: /api/v1
    description: Flowyte control-plane (versioned URI; additive in v1).
security:
  - apiKey: []
tags:
  - name: Agents
    description: The single user-facing entity.
  - name: Support
    description: In-app "Get help" form → support inbox email.
  - name: Skills
    description: >
      Agent capabilities / tools. A skill is ONE atomic action — typically a
      single API call the agent makes in one step (look up an order, create a
      record, send a message, transfer the call). Use a skill when the task is a
      single step. When a task needs several details gathered across turns
      BEFORE acting, build a Playbook (which gathers the inputs and then calls
      skills) — see the Playbooks tag.
  - name: Integrations
    description: Native OAuth integrations.
  - name: Knowledge
    description: RAG knowledge sources & preview.
  - name: Playbooks
    description: >
      Multi-turn conversation scripts — a node graph (gather → confirm → branch)
      the agent follows to collect several inputs IN ORDER across turns before
      acting. Build a playbook when a single skill call isn't enough because the
      agent must gather MANY details first, or run a SEQUENCE of skills, before
      it can finish (e.g. take a full service request: gather the problem,
      address, and time, confirm, THEN file it; qualify a lead; a multi-step
      intake). A playbook does NOT call an integration itself — it owns the
      conversation and holds the state across turns; the actual action is
      performed by the SKILL(s) it gathers the inputs for. So a playbook
      ORCHESTRATES skills. Rule of thumb — one API call → a Skill; "gather N
      things in order, then submit" → a Playbook that drives the conversation
      and calls the skill(s) at the end.
  - name: Variables
    description: >
      The agent-wide interaction-variable registry — DERIVED at read time from
      the agent's playbooks and skills (collect slots, skill output bindings,
      {var} placeholders) and merged with a thin annotation overlay (notes,
      declared type hints, manual declarations). Read-only observation, NEVER a
      gate: it never validates a reference and never affects authoring, publish,
      or runtime behaviour.
  - name: Guardrails
    description: Deterministic guardrail policies & caller verification.
  - name: Numbers
    description: Phone numbers / DIDs.
  - name: SMS
    description: >
      SMS Hub — A2P 10DLC self-serve registration (brand + campaign), the free
      AI compliance review, per-number SMS enablement, and the consent/opt-out
      surface (suppressions, contacts, TCPA proof export). Texting is US-only; a
      number must be SMS-enabled and the org's 10DLC registration active before
      it can send.
  - name: Test
    description: Test, simulate, talk-token, probe.
  - name: Observe
    description: Post-call analytics, conversations, receipts, transcripts.
  - name: Billing
    description: Plans, wallet, usage, fixed phrases.
  - name: Voices
    description: Voice catalog.
  - name: ApiKeys
    description: Developer / API keys.
  - name: Webhooks
    description: Webhook endpoints & deliveries.
  - name: Records
    description: >
      Caller Context Store — pre-synced external records the agent greets a
      caller from, plus the learned object-type field registry. Fed by the
      Zapier app's Create/Update Record action and directly by this REST API;
      read at call time by the context_lookup skill (a sub-100ms local lookup,
      no Zapier round-trip).
  - name: AuditLogs
    description: API/key activity logs.
  - name: Chat
    description: Chat channel — sessions, messages, OpenAI-compatible, widget.
  - name: PublishableKeys
    description: Browser-safe, one-agent publishable keys.
  - name: Widget
    description: Embed widget config + snippet.
  - name: Uploads
    description: Multipart file uploads backing file_id params
  - name: Meta
    description: Platform metadata (language/SKU/tier capabilities).
  - name: Outbound
    description: >
      Outbound voice — contact lists (import + scrub) and campaigns (create,
      launch, pause/resume/cancel). Launch schedules one call attempt per valid
      contact; a background worker dials them. This release dials informational
      campaigns only.
  - name: Team
    description: >
      Team Members — the general "invite a teammate" surface (roster, the
      platform invitations, role changes/removals, and the domain-discovery
      join-request inbox). Distinct from the Flowyte Phone SEAT surface: a team
      invite mints NO softphone seat. Reads → team:read; mutations → team:write
      (owner/admin; staff is role-gated out). Owner-only guards protect owner
      grants/edits and the org's last owner; no self-role-change or
      self-removal.
  - name: Onboarding
    description: >
      Pre-org domain discovery — the signup-flow seam a just-signed-in user hits
      BEFORE they have a workspace, to learn "your coworkers may already be
      here" and ask to join. Engine-root, authenticated by a the platform
      session JWT that may have no active org; tenancy is derived SERVER-SIDE
      from the caller's verified email (anti-enumeration), never from input.
  - name: OAuth2
    description: >
      Flowyte's minimal OAuth2 authorization server. The Flowyte Zapier app is
      the first registered client. The authorization-code grant (confidential
      client, optional PKCE) issues org-scoped SERVICE tokens (flowyte_oat_…)
      that ride the SAME scope matrix as an sk key. The public
      authorize/token/revoke endpoints are served at the ORIGIN ROOT (NOT under
      /api/v1 — see each path's `servers` override); consent + disconnect are
      /api/v1.
paths:
  /escalations:
    post:
      tags:
        - Escalation
      summary: Initiate an outbound escalation (harness-started SMS thread)
      description: >
        Open a BRAND-NEW SMS conversation with a customer and send its first
        message, with the thread owned by the calling connector from the first
        instant. This is the outbound-initiated sibling of the inbound
        escalation path: once the session exists the customer's replies route to
        your harness automatically (the AI does not answer them), so you
        continue on POST /escalations/{id}/messages exactly as you would on any
        escalation you claimed. The session is created ALREADY CLAIMED by the
        calling key and active — you do not race a claim SLA on a thread you
        started. COMPLIANCE (all enforced server-side, none of it optional):

        * There is no `from` field. The sending number is derived from the
        agent's SMS-active number; an agent with none returns 422
        `no_sms_number`.

        * `to` must be a US or Canada (+1) number. The platform's numbers and
        its A2P 10DLC/TCR registration are North American, so any other
        destination is refused 422 `unsupported_country` — nothing is sent.

        * Volume is capped per workspace: a rolling 24-hour ceiling on how many
        conversations a harness may START (200 by default). Past it, 429
        `daily_initiate_cap` carries `details.nextOpen` and `Retry-After`. An
        idempotent replay is answered from the original receipt and is never
        capped.

        * The recipient must have a recorded consent basis — `conversational` (a
        customer who texted you first) or `express` (captured in a call or
        chat). Otherwise 422 `consent_required`. Orgs that hold consent
        out-of-band can be granted a bypass, which is recorded in the compliance
        trail.

        * The first text is business-initiated, so it is quiet-hours gated and
        carries the required brand + "Msg & data rates may apply. Reply HELP for
        help, STOP to opt out." disclosure block, appended by the platform. Do
        not add your own.

        * An initiate that did NOT send is always an explicit error, never a 200
        with delivered:false. A quiet-hours refusal is 409 `quiet_hours` and
        carries `details.nextOpen` — retry after it. IDEMPOTENCY. The key is
        (organization, `client_message_id`) — durable and stable, so a replay
        returns the original receipt (byte for byte) and never sends a second
        text: after the original escalation was resolved, after the 72-hour
        conversation-episode rollover, and against a concurrent duplicate of
        itself. Reusing an id for a DIFFERENT message is 409
        `client_message_id_reused`. WHEN THE OUTCOME IS UNKNOWN. If the carrier
        request fails in a way that does not prove the message stayed home (a
        timeout, a transport failure, a 5xx), the answer is 409
        `send_state_unknown` carrying `details.escalationId` and
        `details.conversationId`. It is the ONLY refusal here that does not
        promise the recipient's phone stayed silent, so do not blindly retry:
        read the thread, or replay the SAME `client_message_id` — a replay
        resolves to the same answer, or to the receipt once the state settles,
        and never sends a second text. Requires the deployment to have the Agent
        Bridge enabled (503 `bridge_disabled`), the org to be enabled for
        harness-initiated conversations (403 `harness_outbound_disabled`) and on
        a Starter plan or higher (403 `plan_required`).
      operationId: initiateEscalation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EscalationInitiateRequest'
            example:
              agentId: agt_4f9c2b7e10a24d51
              destinationId: dest_7a2c
              channel: sms
              to: '+14155551234'
              text: Hi Dana — following up on the quote you asked about yesterday.
              client_message_id: 6f1c2d3e-4a5b-6789-abcd-ef0123456789
      responses:
        '201':
          description: >-
            The conversation was opened and the first message sent (also
            returned on an idempotent replay).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_EscalationInitiated'
              example:
                success: true
                data:
                  escalationId: esc_9f21
                  conversationId: conv_9d2f01
                  state: active
                  messageId: emsg_4c8a
                  seq: 1
                  delivered: true
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
          description: >-
            Refused: no connector key, missing the escalations:initiate scope,
            the org is not enabled for harness-initiated conversations
            (`harness_outbound_disabled`), the org is on PAYG (`plan_required`),
            or the org's 10DLC/TCR campaign is not active (`sms_not_enabled`).
        '409':
          $ref: '#/components/responses/Conflict'
          description: >-
            Refused with an actionable state: `quiet_hours` (outside the
            recipient's legal texting window — `details.nextOpen` carries the
            retry instant), `sms_suppressed` (the recipient replied STOP),
            `conversation_busy` (a teammate's seat, another escalation, or an
            inbound turn being processed right now already owns that thread —
            carries `details.conversationId` and, when known,
            `details.escalationId`), `client_message_id_reused` (that id already
            named a different message), `send_state_unknown` (the send outcome
            is indeterminate — the message MAY have been delivered; do not
            blindly retry, replay the same `client_message_id`), or
            `destination_unavailable` (the destination's AI Harness is
            disconnected).
        '422':
          $ref: '#/components/responses/Unprocessable'
          description: >-
            Refused before sending: `consent_required` (no recorded consent for
            that number), `no_sms_number` (the agent has no SMS-active number),
            `destination_not_ready` (the destination is not active for sms),
            `unsupported_country` (`to` is outside US/Canada), `invalid_to`, or
            `channel_unsupported`.
        '429':
          $ref: '#/components/responses/RateLimited'
          description: >-
            `daily_initiate_cap` — the workspace has started its maximum number
            of harness-initiated conversations in the last 24 hours. Nothing was
            sent. `details.nextOpen` (RFC 3339) and `Retry-After` give the
            instant the next slot frees.
        '502':
          description: >-
            The carrier ANSWERED and rejected the request (a 4xx). Nothing was
            sent and the `client_message_id` has been released, so a corrected
            retry is safe. An unanswered or ambiguous carrier failure is 409
            `send_state_unknown` instead, never this.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
          description: >-
            Unavailable on this deployment: `bridge_disabled` (the Agent Bridge
            is off, so a customer's reply could never reach the harness —
            refused before anything is created) or `sms_not_configured` (SMS
            sending is not wired).
      security:
        - apiKey:
            - escalations:initiate
components:
  schemas:
    EscalationInitiateRequest:
      type: object
      required:
        - agentId
        - destinationId
        - to
        - text
        - client_message_id
      description: >
        A harness-initiated outbound SMS thread. NOTE the absence of a `from`
        field: the sending number is derived server-side from the agent's
        SMS-active number, because an agent structurally cannot text without an
        assigned number.
      properties:
        agentId:
          type: string
          description: >-
            The agent whose SMS-active number the thread is opened on (and whose
            conversation history it joins).
        destinationId:
          type: string
          description: >-
            The escalation destination that will own the conversation. Must be
            status 'active', allow the sms channel, and belong to a CONNECTED AI
            Harness integration.
        channel:
          type: string
          enum:
            - sms
          default: sms
          description: >-
            Only sms can be initiated outbound — chat has no visitor to push to
            until one arrives.
        to:
          type: string
          description: >-
            The recipient in E.164. Must have a recorded consent basis
            (conversational or express) unless the org holds a consent bypass.
          example: '+14155551234'
        text:
          type: string
          description: >-
            The first message. Send the message ONLY — the platform appends the
            required brand prefix and the "Msg & data rates may apply. Reply
            HELP for help, STOP to opt out." disclosure block, sanitizes to
            GSM-7 and applies the 2-segment cap.
        client_message_id:
          type: string
          description: >-
            Idempotency key — a replay returns the original receipt and never
            sends a second text.
    ApiResponse_EscalationInitiated:
      allOf:
        - $ref: '#/components/schemas/ApiResponseBase'
        - type: object
          required:
            - data
          properties:
            data:
              $ref: '#/components/schemas/EscalationInitiated'
    ProblemDetails:
      description: RFC 9457 problem+json.
      type: object
      properties:
        type:
          type: string
          format: uri
          default: about:blank
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        code:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
    ApiResponseBase:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
        message:
          type: string
        errors:
          type: array
          items:
            type: object
            required:
              - field
              - message
            properties:
              field:
                type: string
              message:
                type: string
    EscalationInitiated:
      type: object
      required:
        - escalationId
        - conversationId
        - state
        - delivered
      properties:
        escalationId:
          type: string
          description: >-
            The new session, already claimed by the calling key. Use it for
            every follow-up (/messages, /resolve, ...).
        conversationId:
          type: string
          description: >-
            The SMS conversation the session owns. The customer's replies on it
            route to your harness, not to the AI.
        state:
          type: string
          description: >-
            The session state — 'active' for a fresh initiate; on an idempotent
            replay, whatever the original session's state is now.
        messageId:
          type: string
          description: The conversation-spine id of the sent message.
        seq:
          type: integer
          description: The spine sequence number of the sent message (dedupe/gap-sync key).
        delivered:
          type: boolean
          description: Always true on a 201 — an initiate that did not send is an error
          never a 201 with delivered false.: null
    ApiResponse_Void:
      allOf:
        - $ref: '#/components/schemas/ApiResponseBase'
        - type: object
          properties:
            data:
              type:
                - object
                - 'null'
  responses:
    ValidationError:
      description: Validation failure (errors[] populated on the envelope).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse_Void'
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        WWW-Authenticate:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    Forbidden:
      description: Org mismatch / RLS / insufficient scope / origin not allowed.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    Conflict:
      description: Conflict (duplicate name, publish race, version mismatch, no diff).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    Unprocessable:
      description: Semantically invalid (e.g. language not in SKU's STT set).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    RateLimited:
      description: Rate-limited (Redis limiter).
      headers:
        RateLimit-Limit:
          schema:
            type: integer
        RateLimit-Remaining:
          schema:
            type: integer
        RateLimit-Reset:
          schema:
            type: integer
        Retry-After:
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    ServiceUnavailable:
      description: A required dependency is not configured/available (fails closed).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
  securitySchemes:
    apiKey:
      type: oauth2
      description: >
        Flowyte secret API key (`Authorization: Bearer flowyte_sk_live_…`).
        Scope-gated; is scoped to your organization — a key can never reach
        another tenant. The listed scopes in each operation's `apiKey`
        requirement are the scopes that key must hold. The `tokenUrl` is
        nominal: keys are minted in the dashboard.
      flows:
        clientCredentials:
          tokenUrl: /api/v1/api-keys
          scopes:
            agents:read: Read agents.
            agents:write: Create/update/delete agents, publish, rollback.
            knowledge:read: Read knowledge sources & preview.
            knowledge:write: Add/remove knowledge sources, uploads.
            skills:read: Read skills & skill-types.
            skills:write: Create/update/delete skills.
            playbooks:read: Read playbooks & graphs.
            playbooks:write: Create/update/delete playbooks & graphs.
            guardrails:read: Read guardrail policies & caller-verification.
            guardrails:write: Update guardrail policies & caller-verification.
            numbers:read: Read phone numbers / search availability.
            numbers:write: Purchase / assign / release numbers.
            sms:read: Read the org's SMS (10DLC) registration status and numbers.
            sms:write: Save/submit the 10DLC registration and toggle numbers for SMS.
            outbound:read: Read outbound contact lists and campaigns.
            outbound:write: >-
              Create/import contact lists, create/launch/pause/resume/cancel
              outbound campaigns, and enqueue single outbound calls.
            integrations:read: Read connected native integrations (status only — never tokens).
            integrations:write: Discover schemas, set data scoping, and disconnect a connection.
            integrations:connect: >-
              Connect a data source (submit credentials / begin OAuth) — a
              SEPARATE, higher-privilege scope because connecting INGESTS
              credentials and opens a new egress path; a discover/scope/author
              key need not carry it.
            records:read: >-
              Read the pre-synced Caller Context Store records and object-type
              field registry.
            records:write: >-
              Upsert / delete / bulk-import / purge caller-context records and
              edit type metadata.
            calls:read: Read conversations, receipts, transcripts, analytics.
            analytics:read: >-
              Read the Observe history list, per-agent analytics (the
              answer-rate summary), the raw knowledge-gap list, and the metric
              catalog + per-metric queries + metric drill-downs.
            analytics:write: >-
              Curate knowledge gaps (dismiss / mark in-progress). [M4 —
              reserved]
            dashboards:read: List and read saved Observe reporting dashboards.
            dashboards:write: >-
              Create, update (full-document replace), and delete Observe
              reporting dashboards.
            reports:read: List and read Observe scheduled reports (report schedules).
            reports:write: >-
              Create, update, and delete Observe scheduled reports (a delete
              stops a recurring digest).
            billing:read: Read plans, wallet, usage.
            audit:read: Read API/key activity logs.
            webhooks:write: Manage webhook endpoints.
            keys:write: Manage secret API keys.
            chat:read: Read chat sessions & messages.
            chat:write: Create chat sessions & send messages (server-side).
            widgets:read: Read widget config & embed snippet.
            widgets:write: Update widget config.
            pubkeys:read: Read publishable keys.
            pubkeys:write: Manage publishable keys.
            phone:read: >-
              Read Flowyte Phone config — settings, ring targets/queues +
              rosters, contacts, block-list, own presence.
            phone:write: >-
              Update Flowyte Phone config — settings, ring targets/queues +
              members, contacts, block-list.
            presence:write: Set your own softphone presence (available/away/dnd/…).
            team:read: Read the team — softphone seats and the team presence roster.
            team:write: >-
              Manage softphone seats — invite, update, and deactivate team
              members.
            escalation_destinations:read: Read external-agent (AI Harness) escalation destinations.
            escalation_destinations:write: >-
              Create, update, delete, and rotate the signing secret of
              escalation destinations.
            escalation_policies:read: Read an agent's escalation routing policy. [ — reserved]
            escalation_policies:write: Update an agent's escalation routing policy. [ — reserved]
            escalations:read: >-
              Read escalation sessions and their message history (connector). [
              — reserved]
            escalations:claim: >-
              Claim an escalation session and extend its lease (connector). [ —
              reserved]
            escalations:respond: >-
              Post messages into a claimed escalation session (connector). [ —
              reserved]
            escalations:resolve: >-
              Resolve, return, or request human takeover of an escalation
              (connector). [ — reserved]
            escalations:initiate: >-
              Start a NEW outbound SMS conversation with a customer (connector).
              Deliberately separate from escalations:respond — the other scopes
              work a conversation the platform handed over; this one texts a
              person who never contacted you on that thread. Also requires the
              org to be enabled for harness-initiated conversations.

````