openapi: 3.1.0
info:
  title: Lumika Connectors API
  version: "2026-09-01"
  description: >
    Reviewed, machine-readable v1 route contract for Lumika Connectors. Zod
    schemas in @lumika/connectors-contract remain the runtime validation source;
    compatibility tests pin this hand-maintained document to those schemas and
    to every implemented public route. Clients must tolerate additive fields
    and open-enum values as documented.
servers:
  - url: https://api.connectors.lumika.ai/v1
security:
  - bearer: []
x-lumika-feature-gates:
  hosted-connect:
    environment: CONNECTORS_HOSTED_CONNECT_ENABLED
    default: false
    description: Creation, start, completion, and provider callbacks reject before state mutation or provider I/O while disabled; status, read-only preview, and cleanup remain available.
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "sk_live_… or sk_test_… app key" }
  parameters:
    Version: { name: Connectors-Version, in: header, schema: { type: string, example: "2026-09-01" } }
    IdempotencyKey: { name: Idempotency-Key, in: header, required: true, schema: { type: string, maxLength: 255 } }
    Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
    StartingAfter: { name: starting_after, in: query, schema: { type: string } }
  schemas:
    Id: { type: string, description: "Opaque, type-prefixed connector id; clients must not parse its payload", example: "con_01J8Z…" }
    MessageId:
      type: string
      minLength: 6
      maxLength: 128
      pattern: '^msg_[A-Za-z0-9][A-Za-z0-9_-]*$'
      description: "Opaque Message id"
    ParticipantId:
      type: string
      minLength: 6
      maxLength: 128
      pattern: '^prt_[A-Za-z0-9][A-Za-z0-9_-]*$'
      description: "Opaque Participant id"
    GrantId:
      type: string
      minLength: 6
      maxLength: 128
      pattern: '^grt_[A-Za-z0-9][A-Za-z0-9_-]*$'
      description: "Opaque exact Grant id"
    IdentityAssurance:
      type: string
      examples: [e164_verified, provider_id, email_address, anonymous]
      description: Open enum; clients must tolerate additive identity-assurance values.
    ApiKeyCapability:
      type: string
      examples: ["connections:read", "connections:write", "installations:write", "messages:read", "messages:send", "participants:read", "events:read", "webhooks:read", "webhooks:write", "test:write", "keys:write"]
      description: Open enum scoped to an AppEnvironment API key. It is distinct from a Connection Grant scope.
    ApiKey:
      type: object
      required: [id, name, hint, capabilities, status, expires_at, last_used_at, created_at]
      properties:
        id: { type: string, pattern: '^key_', description: Opaque API-key record id; never the credential itself }
        name: { type: string, minLength: 1, maxLength: 100 }
        hint: { type: string, minLength: 1, maxLength: 128, description: Non-secret display hint }
        capabilities: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/ApiKeyCapability" } }
        status: { type: string, enum: [active, disabled, revoked, expired] }
        expires_at: { type: [string, "null"], format: date-time }
        last_used_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        rotated_from_id: { type: string, pattern: '^key_', description: Present on a replacement key created by rotation }
    CreateApiKeyRequest:
      type: object
      required: [name, capabilities]
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        capabilities:
          type: array
          minItems: 1
          maxItems: 50
          uniqueItems: true
          items: { $ref: "#/components/schemas/ApiKeyCapability" }
        expires_at: { type: string, format: date-time }
    RotateApiKeyRequest:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        expires_at: { type: string, format: date-time }
    ApiKeyCreatedResponse:
      allOf:
        - { $ref: "#/components/schemas/ApiKey" }
        - type: object
          required: [secret, secret_shown]
          properties:
            secret: { type: string, pattern: '^sk_(test|live)_[A-Za-z0-9_-]{16,}$', description: Plaintext shown in this response only }
            secret_shown: { type: boolean, const: true }
    ApiKeyList:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/ApiKey" } }
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"], pattern: '^key_' }
    ConnectionKind:
      type: string
      examples: [meta_messenger, meta_instagram, meta_whatsapp, twilio_sms, twilio_voice, telegram, ses_email, web, test]
      description: Open enum — examples are not an exhaustive allowed list; clients must tolerate new values.
    Error:
      type: object
      properties:
        error:
          type: object
          required: [type, code, message, retryable, request_id]
          properties:
            type: { type: string, enum: [invalid_request, auth, not_found, conflict, policy, rate_limited, provider, internal] }
            code: { type: string }
            message: { type: string }
            retryable: { type: boolean }
            details: { type: object, additionalProperties: true }
            request_id: { type: string }
    Capabilities:
      type: object
      required: [kind, markup, text_max, text_unit, attachments, initiation, window, typing, read_receipts, delivery_receipts, streaming, templates, voice, identity, opt_out, outbound_observation]
      properties:
        kind: { $ref: "#/components/schemas/ConnectionKind" }
        markup: { type: string, enum: [none, whatsapp, html_subset, markdown_lite] }
        text_max: { type: integer }
        text_unit: { type: string, enum: [utf8_bytes, unicode_code_points] }
        attachments:
          type: object
          required: [kinds, max_bytes, max_per_message]
          properties:
            kinds: { type: array, items: { type: string, enum: [image, video, audio, document, card] } }
            max_bytes: { type: integer }
            max_per_message: { type: integer }
        initiation: { type: string, enum: [business_allowed, template_only, after_participant, participant_only] }
        window:
          oneOf:
            - type: "null"
            - type: object
              properties:
                hours: { type: integer }
                extensions: { type: array, items: { type: string, enum: [human_agent, template] } }
        typing: { type: boolean }
        read_receipts: { type: boolean }
        delivery_receipts: { type: boolean }
        streaming: { type: boolean }
        templates: { type: boolean }
        voice: { type: boolean }
        transport:
          type: string
          enum: [chat, sms, voice, email, web, test]
          description: Stable transport family; optional for v1 compatibility.
        recipient_address_kinds:
          type: array
          uniqueItems: true
          items: { type: string, enum: [phone, telegram_chat_id, email, participant_id] }
          description: Explicit supported start-conversation address forms; optional for v1 compatibility.
        calls:
          oneOf:
            - type: "null"
            - type: object
              required: [inbound, outbound, forwarding, voicemail, recording, transcription, media_streaming]
              properties:
                inbound: { type: boolean }
                outbound: { type: boolean }
                forwarding: { type: boolean }
                voicemail: { type: boolean }
                recording: { type: boolean }
                transcription: { type: boolean }
                media_streaming: { type: boolean }
          description: Structured call support. Null or absent for message-only Connections; voice remains the v1 compatibility flag.
        threading:
          type: object
          required: [model]
          properties:
            model: { type: string, enum: [participant, message_references] }
            recipient_limit: { type: integer, minimum: 1, maximum: 50 }
          description: Absent is the backward-compatible participant-thread model. Message-reference threading is used by email transports.
        identity: { $ref: "#/components/schemas/IdentityAssurance" }
        opt_out: { type: string, enum: [keywords, command, none] }
        outbound_observation: { type: string, enum: [complete, connector_only, best_effort, none] }
    AppInstallation:
      type: object
      required: [id, external_ref, business, status, send_mode]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        external_ref: { type: string, maxLength: 128 }
        business:
          type: object
          required: [id, name]
          properties: { id: { $ref: "#/components/schemas/Id" }, name: { type: string } }
        status: { type: string, enum: [pending, active, disabled, suspended, revoked] }
        send_mode: { type: string, enum: [human, automated], default: human }
        automation_approved_at: { type: [string, "null"], format: date-time }
    ConnectSessionRequest:
      type: object
      required: [kind, installation_id, return_url]
      properties:
        kind:
          oneOf:
            - { const: any }
            - { $ref: "#/components/schemas/ConnectionKind" }
        installation_id: { type: string, pattern: '^ins_' }
        return_url: { type: string, format: uri, description: Exact HTTPS AppEnvironment allowlist member }
        scopes:
          type: array
          minItems: 1
          uniqueItems: true
          items: { type: string }
        reconnect_connection_id: { type: string, pattern: '^con_' }
        business_hint:
          type: object
          properties:
            name: { type: string, maxLength: 200 }
            email: { type: string, format: email, maxLength: 320 }
          description: Display hint only; never identity or ownership evidence.
        client_reference: { type: string, minLength: 1, maxLength: 128 }
    ConnectSessionCreated:
      type: object
      required: [id, url, expires_at]
      properties:
        id: { type: string, pattern: '^ses_' }
        url: { type: string, format: uri, description: One-use hosted launch URL; never log or forward it }
        expires_at: { type: string, format: date-time }
    ConnectSessionResult:
      type: object
      required: [connection_id, grant_id]
      properties:
        connection_id: { type: string, pattern: '^con_' }
        grant_id: { type: string, pattern: '^grt_' }
    ConnectSession:
      type: object
      required: [id, status, expires_at, results]
      properties:
        id: { type: string, pattern: '^ses_' }
        client_reference: { type: [string, "null"], maxLength: 128 }
        status: { type: string, enum: [pending, granted, cancelled, failed, expired] }
        expires_at: { type: string, format: date-time }
        results:
          type: array
          items: { $ref: "#/components/schemas/ConnectSessionResult" }
        error: { type: [string, "null"] }
    HostedConnectDisclosure:
      type: object
      additionalProperties: false
      required: [version, text, text_hash, requires_acknowledgment]
      properties:
        version: { type: string, minLength: 1, maxLength: 64 }
        text: { type: string, minLength: 1, maxLength: 2000 }
        text_hash: { type: string, pattern: '^[a-f0-9]{64}$' }
        requires_acknowledgment: { type: boolean, const: true }
    HostedConnectPreview:
      type: object
      additionalProperties: false
      required: [session_id, app, requested_kind, allowed_kinds, expires_at, disclosure, csrf_token, start_url]
      properties:
        session_id: { type: string, pattern: '^ses_' }
        app:
          type: object
          additionalProperties: false
          required: [name, logo_url, privacy_url]
          properties:
            name: { type: string, minLength: 1, maxLength: 120 }
            logo_url: { type: [string, "null"], format: uri }
            privacy_url: { type: [string, "null"], format: uri }
        requested_kind:
          oneOf:
            - { const: any }
            - { $ref: "#/components/schemas/ConnectionKind" }
        allowed_kinds:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/ConnectionKind" }
        expires_at: { type: string, format: date-time }
        disclosure: { $ref: "#/components/schemas/HostedConnectDisclosure" }
        csrf_token: { type: string, pattern: '^cx_csrf_[A-Za-z0-9_-]{32,128}$' }
        start_url: { type: string, format: uri }
    HostedConnectStartRequest:
      type: object
      additionalProperties: false
      required: [csrf_token, acknowledgment]
      properties:
        kind: { $ref: "#/components/schemas/ConnectionKind" }
        csrf_token: { type: string, pattern: '^cx_csrf_[A-Za-z0-9_-]{32,128}$' }
        acknowledgment:
          type: object
          additionalProperties: false
          required: [accepted, text_hash]
          properties:
            accepted: { type: boolean, const: true }
            text_hash: { type: string, pattern: '^[a-f0-9]{64}$' }
    HostedConnectStep:
      type: object
      additionalProperties: false
      required: [session_id, kind, app, expires_at, step]
      properties:
        session_id: { type: string, pattern: '^ses_' }
        kind: { $ref: "#/components/schemas/ConnectionKind" }
        app:
          type: object
          additionalProperties: false
          required: [name, logo_url, privacy_url]
          properties:
            name: { type: string, minLength: 1, maxLength: 120 }
            logo_url: { type: [string, "null"], format: uri }
            privacy_url: { type: [string, "null"], format: uri }
        expires_at: { type: string, format: date-time }
        step:
          oneOf:
            - type: object
              additionalProperties: false
              required: [type, url]
              properties:
                type: { const: redirect }
                url: { type: string, format: uri }
            - type: object
              additionalProperties: false
              required: [type, fields, complete_url]
              properties:
                type: { const: form }
                fields: { type: object, additionalProperties: true }
                complete_url: { type: string, format: uri }
    HostedConnectCompletion:
      type: object
      additionalProperties: false
      required: [session_id, status, redirect_url, results]
      properties:
        session_id: { type: string, pattern: '^ses_' }
        status: { type: string, const: granted }
        redirect_url: { type: string, format: uri }
        results:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/ConnectSessionResult" }
    Grant:
      type: object
      required: [id, installation_id, connection_id, scopes, effective_at, expires_at, granted_at, revoked_at, revision]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        installation_id: { $ref: "#/components/schemas/Id" }
        connection_id: { $ref: "#/components/schemas/Id" }
        scopes: { type: array, items: { type: string } }
        effective_at: { type: string, format: date-time }
        expires_at: { type: [string, "null"], format: date-time }
        granted_at: { type: string, format: date-time }
        revoked_at: { type: [string, "null"], format: date-time }
        revision: { type: integer, minimum: 0 }
    Connection:
      type: object
      required: [id, kind, status, livemode, business, installation, external_account_id, display, grant, capabilities, health, created_at, updated_at]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        kind: { $ref: "#/components/schemas/ConnectionKind" }
        status: { type: string, enum: [pending, active, needs_reauth, revoked, disabled] }
        livemode: { type: boolean }
        business:
          type: object
          properties: { id: { $ref: "#/components/schemas/Id" }, name: { type: string } }
        installation: { $ref: "#/components/schemas/AppInstallation" }
        external_account_id: { type: string }
        display:
          type: object
          properties: { name: { type: string }, username: { type: [string, "null"] }, avatar_url: { type: [string, "null"], format: uri } }
        grant: { $ref: "#/components/schemas/Grant" }
        capabilities: { $ref: "#/components/schemas/Capabilities" }
        health:
          type: object
          properties:
            state: { type: string, enum: [ok, degraded, needs_reauth, revoked] }
            checked_at: { type: string, format: date-time }
            token_expires_at: { type: [string, "null"], format: date-time }
            last_inbound_at: { type: [string, "null"], format: date-time }
            last_error: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Window:
      type: object
      required: [state]
      properties:
        state: { type: string, enum: [open, extended, closed, none] }
        expires_at: { type: [string, "null"], format: date-time }
        extensions: { type: array, items: { type: string } }
    Participant:
      type: object
      required: [id, connection_id, external_id, identity]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        connection_id: { $ref: "#/components/schemas/Id" }
        external_id: { type: string }
        identity: { $ref: "#/components/schemas/IdentityAssurance" }
        name: { type: [string, "null"] }
        username: { type: [string, "null"] }
        avatar_url: { type: [string, "null"], format: uri }
        phone: { type: [string, "null"] }
        locale: { type: [string, "null"] }
        opted_out: { type: boolean }
        opted_out_at: { type: [string, "null"], format: date-time }
        profile_fetched_at: { type: [string, "null"], format: date-time }
    Conversation:
      type: object
      required: [id, connection_id, external_ref, livemode, participant, window, control, initiated_by, last_inbound_at, last_outbound_at, unread, created_at]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        connection_id: { $ref: "#/components/schemas/Id" }
        external_ref: { type: string }
        livemode: { type: boolean }
        participant: { $ref: "#/components/schemas/Participant" }
        window: { $ref: "#/components/schemas/Window" }
        control:
          type: object
          required: [installation_id, lease_id, fencing_token, expires_at]
          properties:
            state: { type: string, enum: [available, held_by_caller, held_by_other] }
            installation_id: { type: [string, "null"] }
            lease_id: { type: [string, "null"] }
            fencing_token: { type: [string, "null"] }
            expires_at: { type: [string, "null"], format: date-time }
        initiated_by: { type: string, enum: [participant, app, unknown] }
        last_inbound_at: { type: [string, "null"], format: date-time }
        last_outbound_at: { type: [string, "null"], format: date-time }
        unread: { type: integer }
        revision: { type: integer, minimum: 0, maximum: 9007199254740991, description: "Authoritative Conversation snapshot revision when its origin is inside the caller's Grant horizon. Omitted from a clamped pre-horizon parent projection; a missing revision may bootstrap an absent binding but must not overwrite existing window/control projections." }
        created_at: { type: string, format: date-time }
        threading:
          type: object
          required: [model]
          properties:
            model: { type: string, enum: [participant, message_references] }
    Attachment:
      type: object
      required: [id, kind, mime, bytes]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        kind: { type: string, enum: [image, video, audio, document, card] }
        mime: { type: string }
        bytes: { type: integer }
        url: { type: string, format: uri, description: "Signed, short-lived" }
        url_expires_at: { type: string, format: date-time }
        name: { type: [string, "null"] }
    AttachmentRef:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url: { type: string, format: uri, pattern: '^https://' }
        kind: { type: string, enum: [image, video, audio, document, card] }
    MessageRecipient:
      type: object
      additionalProperties: false
      required: [participant_id]
      properties:
        participant_id: { $ref: "#/components/schemas/ParticipantId" }
        address: { type: string, format: email, maxLength: 320 }
    MessageRecipients:
      type: object
      additionalProperties: false
      required: [to, cc]
      description: At most 50 unique Participants may appear across To, CC and BCC. BCC is omitted from projections that are not authorized to see it.
      properties:
        to:
          type: array
          maxItems: 50
          items: { $ref: "#/components/schemas/MessageRecipient" }
        cc:
          type: array
          maxItems: 50
          items: { $ref: "#/components/schemas/MessageRecipient" }
        bcc:
          type: array
          maxItems: 50
          items: { $ref: "#/components/schemas/MessageRecipient" }
    MessageThread:
      type: object
      additionalProperties: false
      required: [root_message_id, parent_message_id]
      properties:
        root_message_id: { $ref: "#/components/schemas/MessageId" }
        parent_message_id:
          oneOf:
            - { $ref: "#/components/schemas/MessageId" }
            - { type: "null" }
        transport_message_id:
          type: [string, "null"]
          maxLength: 998
          pattern: '^<[^<>@\s]+@[^<>@\s]+>$'
    Message:
      type: object
      required: [id, conversation_id, direction, status, attachments, sent_by, author, occurred_at, created_at]
      dependentRequired:
        participant_id: [participant_role]
        participant_role: [participant_id]
      properties:
        id: { $ref: "#/components/schemas/MessageId" }
        conversation_id: { $ref: "#/components/schemas/Id" }
        direction: { type: string, enum: [inbound, outbound] }
        status: { type: string, enum: [received, queued, sending, sent, delivered, read, delivery_unknown, failed] }
        provider_message_id: { type: [string, "null"] }
        participant_id: { $ref: "#/components/schemas/ParticipantId" }
        participant_role: { type: string, enum: [sender, primary_recipient] }
        text: { type: [string, "null"] }
        subject: { type: [string, "null"], maxLength: 3992 }
        cc:
          type: array
          maxItems: 50
          items: { type: string, format: email, maxLength: 320 }
        bcc:
          type: array
          maxItems: 50
          items: { type: string, format: email, maxLength: 320 }
        reply_to:
          oneOf:
            - { $ref: "#/components/schemas/MessageId" }
            - { type: "null" }
        recipients: { $ref: "#/components/schemas/MessageRecipients" }
        thread: { $ref: "#/components/schemas/MessageThread" }
        attachments: { type: array, items: { $ref: "#/components/schemas/Attachment" } }
        sent_by:
          oneOf:
            - type: "null"
            - type: string
              enum: [native, unknown_external_app]
            - type: object
              required: [app_id, installation_id, app_name]
              properties: { app_id: { type: string }, installation_id: { type: string }, app_name: { type: string } }
        author:
          type: [object, "null"]
          properties: { type: { type: string, enum: [human, ai, unknown] }, ref: { type: string } }
        tag: { type: [string, "null"], enum: [human_agent, null] }
        template: { type: [object, "null"], additionalProperties: true }
        failure:
          type: [object, "null"]
          properties: { code: { type: string }, reason: { type: string }, retryable: { type: boolean }, provider_code: { type: [string, integer, "null"] } }
        provider_data: { type: object, additionalProperties: true }
        metadata: { type: object, additionalProperties: true }
        client_message_id: { type: [string, "null"] }
        occurred_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
    MessageAttempt:
      type: object
      required: [id, number, state, at, error]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        number: { type: integer, minimum: 1 }
        state: { type: string, enum: [claimed, succeeded, retryable, delivery_unknown, failed] }
        at: { type: string, format: date-time }
        error: { type: [object, "null"], additionalProperties: true }
    MessageWithAttempts:
      allOf:
        - { $ref: "#/components/schemas/Message" }
        - type: object
          required: [attempts]
          properties:
            attempts: { type: array, items: { $ref: "#/components/schemas/MessageAttempt" } }
    SentBy:
      oneOf:
        - { type: string, enum: [native, unknown_external_app] }
        - type: object
          required: [app_id, installation_id, app_name]
          properties:
            app_id: { $ref: "#/components/schemas/Id" }
            installation_id: { $ref: "#/components/schemas/Id" }
            app_name: { type: string }
    MessageStatusData:
      type: object
      required: [message_id, client_message_id, conversation_id, sent_by, at]
      properties:
        message_id: { $ref: "#/components/schemas/Id" }
        client_message_id: { type: string, maxLength: 128 }
        conversation_id: { $ref: "#/components/schemas/Id" }
        sent_by: { $ref: "#/components/schemas/SentBy" }
        at: { type: string, format: date-time }
        watermark_at: { type: string, format: date-time }
    MessageFailedData:
      type: object
      required: [message_id, client_message_id, conversation_id, sent_by, failure]
      properties:
        message_id: { $ref: "#/components/schemas/Id" }
        client_message_id: { type: string, maxLength: 128 }
        conversation_id: { $ref: "#/components/schemas/Id" }
        sent_by: { $ref: "#/components/schemas/SentBy" }
        failure:
          type: object
          required: [code, reason, retryable]
          properties:
            code: { type: string }
            reason: { type: string }
            retryable: { type: boolean, const: false }
            provider_code: { type: [string, integer, "null"] }
    MessageDeliveryUnknownData:
      type: object
      required: [message_id, client_message_id, conversation_id, sent_by, attempt_id, reason]
      properties:
        message_id: { $ref: "#/components/schemas/Id" }
        client_message_id: { type: string, maxLength: 128 }
        conversation_id: { $ref: "#/components/schemas/Id" }
        sent_by: { $ref: "#/components/schemas/SentBy" }
        attempt_id: { $ref: "#/components/schemas/Id" }
        reason: { type: string }
    SendMessageRequest:
      type: object
      required: [author, client_message_id]
      properties:
        text: { type: string }
        subject: { type: string, maxLength: 998 }
        cc:
          type: array
          maxItems: 50
          items: { type: string, format: email, maxLength: 320 }
        bcc:
          type: array
          maxItems: 50
          items: { type: string, format: email, maxLength: 320 }
        attachments:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/AttachmentRef" }
        author:
          type: object
          required: [type]
          properties: { type: { type: string, enum: [human, ai] }, ref: { type: string } }
        client_message_id: { type: string, maxLength: 128 }
        expected_grant_id:
          allOf: [ { $ref: "#/components/schemas/GrantId" } ]
          description: Optional exact-Grant precondition; a replacement is reported as not found.
        control:
          type: object
          required: [lease_id, fencing_token]
          properties:
            lease_id: { type: string }
            fencing_token: { type: string }
        tag: { type: string, enum: [human_agent] }
        template:
          type: object
          properties: { name: { type: string }, language: { type: string }, params: { type: array, items: {} } }
        reply_to: { $ref: "#/components/schemas/MessageId" }
        metadata: { type: object, additionalProperties: true }
    ConversationRecipient:
      type: object
      description: Exactly one supported address must be supplied.
      properties:
        phone: { type: string, pattern: '^\+[1-9][0-9]{6,14}$', description: E.164 phone number }
        telegram_chat_id: { type: string, minLength: 1, maxLength: 128 }
        email: { type: string, format: email, maxLength: 320 }
        participant_id: { $ref: "#/components/schemas/ParticipantId" }
      oneOf:
        - required: [phone]
        - required: [telegram_chat_id]
        - required: [email]
        - required: [participant_id]
    StartConversationRequest:
      type: object
      required: [connection_id, to, author, client_message_id]
      properties:
        connection_id: { $ref: "#/components/schemas/Id" }
        to: { $ref: "#/components/schemas/ConversationRecipient" }
        text: { type: string }
        subject: { type: string, maxLength: 998 }
        cc:
          type: array
          maxItems: 50
          items: { type: string, format: email, maxLength: 320 }
        bcc:
          type: array
          maxItems: 50
          items: { type: string, format: email, maxLength: 320 }
        template: { type: object, additionalProperties: true }
        attachments:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/AttachmentRef" }
        author:
          type: object
          required: [type]
          properties: { type: { type: string, enum: [human, ai] }, ref: { type: string } }
        client_message_id: { type: string, maxLength: 128 }
        expected_grant_id:
          allOf: [ { $ref: "#/components/schemas/GrantId" } ]
          description: Optional exact-Grant precondition; a replacement is reported as not found.
        control_request:
          type: object
          required: [ttl_seconds]
          properties:
            ttl_seconds: { type: integer, minimum: 5, maximum: 300 }
            expected_fencing_token: { type: string }
    CallAddress:
      type: object
      properties:
        phone: { type: string, pattern: '^\+[1-9][0-9]{6,14}$', description: E.164 phone number }
        participant_id: { type: string, pattern: '^prt_[A-Za-z0-9][A-Za-z0-9_-]*$' }
      oneOf:
        - required: [phone]
          not: { required: [participant_id] }
        - required: [participant_id]
          not: { required: [phone] }
    StartCallRequest:
      type: object
      required: [connection_id, to, client_reference]
      properties:
        connection_id: { type: string, pattern: '^con_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        to: { $ref: "#/components/schemas/CallAddress" }
        client_reference: { type: string, minLength: 1, maxLength: 128 }
        expected_grant_id:
          allOf: [ { $ref: "#/components/schemas/GrantId" } ]
          description: Optional exact-Grant precondition; a replacement is reported as not found.
        mode:
          type: string
          examples: [connect, forward, relay]
          default: connect
          description: Open enum. The selected Connection must explicitly support the requested mode.
        timeout_seconds: { type: integer, minimum: 5, maximum: 600 }
        record: { type: boolean, default: false }
        metadata: { type: object, additionalProperties: true }
    StartCallResponse:
      type: object
      required: [id, client_reference, state]
      properties:
        id: { type: string, pattern: '^cal_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        client_reference: { type: string, minLength: 1, maxLength: 128 }
        state: { type: string, const: queued }
      description: Privacy-safe durable admission acknowledgement. Reconcile the full Call with GET /calls/{id}.
    Call:
      type: object
      required: [id, connection_id, provider_call_id, participant_id, direction, state, from, to, started_at, answered_at, ended_at, duration_seconds, client_reference]
      properties:
        id: { type: string, pattern: '^cal_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        connection_id: { type: string, pattern: '^con_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        provider_call_id: { type: [string, "null"], minLength: 1, maxLength: 512 }
        participant_id: { type: string, pattern: '^prt_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        direction: { type: string, enum: [inbound, outbound] }
        state:
          type: string
          examples: [queued, initiated, ringing, answered, completed, busy, no_answer, canceled, failed, delivery_unknown]
          description: Open enum; consumers must tolerate additive values.
        from: { type: string, pattern: '^\+[1-9][0-9]{6,14}$' }
        to: { type: string, pattern: '^\+[1-9][0-9]{6,14}$' }
        started_at: { type: string, format: date-time }
        answered_at: { type: [string, "null"], format: date-time }
        ended_at: { type: [string, "null"], format: date-time }
        duration_seconds: { type: [integer, "null"], minimum: 0 }
        client_reference:
          type: [string, "null"]
          minLength: 1
          maxLength: 128
          description: Present only to the originating AppEnvironment; peer readers receive null.
        failure:
          type: [object, "null"]
          required: [code, reason, retryable]
          properties:
            code: { type: string, minLength: 1 }
            reason: { type: string, minLength: 1 }
            retryable: { type: boolean, const: false }
        provider_data: { type: object, additionalProperties: true }
    CallEventData:
      type: object
      required: [call]
      properties:
        call: { $ref: "#/components/schemas/Call" }
    CallRecordingReadyData:
      type: object
      required: [call_id, recording]
      properties:
        call_id: { type: string, pattern: '^cal_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        recording:
          type: object
          required: [id, status]
          properties:
            id: { type: string, minLength: 1, maxLength: 512 }
            url: { type: string, format: uri }
            duration_seconds: { type: integer, minimum: 0 }
            status: { type: string, minLength: 1 }
    CallTranscriptReadyData:
      type: object
      required: [call_id, transcript]
      properties:
        call_id: { type: string, pattern: '^cal_[A-Za-z0-9][A-Za-z0-9_-]*$' }
        transcript:
          type: object
          required: [text]
          properties:
            text: { type: string }
            language: { type: string, maxLength: 35 }
    WebhookEndpoint:
      type: object
      required: [id, url, event_types, status, created_at]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        url: { type: string, format: uri }
        event_types:
          oneOf:
            - { type: string, enum: ["*"] }
            - { type: array, items: { type: string } }
        status: { type: string, enum: [active, paused] }
        secret_generation: { type: string, description: Opaque active signing-secret generation }
        secret: { type: string, description: "whsec_… — only on create and rotate" }
        description: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
    RotateWebhookSecretResponse:
      type: object
      required: [secret, secret_generation, previous_secret_expires_at]
      properties:
        secret: { type: string, pattern: '^whsec_', description: Returned on the original response and a same-key idempotent replay; absent from list/get }
        secret_generation: { type: string, description: Opaque generation used for safe idempotent replay }
        previous_secret_expires_at: { type: string, format: date-time }
    TestWebhookResponse:
      type: object
      required: [event_id]
      properties:
        event_id: { $ref: "#/components/schemas/Id" }
    WebhookDelivery:
      type: object
      required: [id, event_id, state, attempts, last_http_status, last_error, request_headers, response_excerpt, created_at, delivered_at]
      properties:
        id: { type: string, description: Opaque delivery id }
        event_id: { $ref: "#/components/schemas/Id" }
        state: { type: string, enum: [pending, claimed, delivered, failed] }
        attempts: { type: integer, minimum: 0 }
        last_http_status: { type: [integer, "null"] }
        last_error: { type: [string, "null"] }
        request_headers:
          type: [object, "null"]
          additionalProperties: { type: string }
        response_excerpt: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        delivered_at: { type: [string, "null"], format: date-time }
    WebhookDeliveryList:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/WebhookDelivery" } }
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }
    RecoverWebhookResponse:
      type: object
      required: [queued, has_more, next_cursor]
      properties:
        queued: { type: integer, minimum: 0, description: "Delivery rows inserted or terminal rows reset; pending and claimed rows are left intact" }
        has_more: { type: boolean }
        next_cursor: { $ref: "#/components/schemas/RecoveryCursor" }
    EventFeedCursor:
      type: string
      maxLength: 1024
      pattern: '^evcur_[A-Za-z0-9_-]+$'
      description: Opaque durable checkpoint bound to the AppEnvironment and normalized types, connection_id and since filters. Never parse or synthesize it.
    RecoveryCursor:
      type: string
      maxLength: 1024
      pattern: '^recur_[A-Za-z0-9_-]+$'
      description: Opaque finite recovery checkpoint bound to the AppEnvironment, endpoint, normalized since and endpoint event subscriptions; includes the initial committed high-water.
    EventList:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Event" } }
        has_more: { type: boolean }
        next_cursor:
          allOf: [ { $ref: "#/components/schemas/EventFeedCursor" } ]
          description: Always non-null, including empty or exhausted responses. Persist only after consuming the page; continue polling with the same filters.
    TestProviderFailureRequest:
      type: object
      required: [connection_id, code, retryable]
      properties:
        connection_id: { $ref: "#/components/schemas/Id" }
        code: { type: string }
        message: { type: string }
        retryable: { type: boolean }
        ambiguous: { type: boolean, default: false }
    TestInboundResponse:
      type: object
      required: [accepted, receipt_ids]
      properties:
        accepted: { type: integer, minimum: 0 }
        receipt_ids: { type: array, items: { type: string, description: Opaque provider-receipt id } }
    CreateTestConnectionRequest:
      type: object
      required: [kind_emulated, external_ref]
      properties:
        kind_emulated: { $ref: "#/components/schemas/ConnectionKind" }
        external_ref: { type: string, minLength: 1, maxLength: 128 }
    Event:
      type: object
      required: [id, type, occurred_at, created_at, api_version, livemode, business_id, installation_id, connection_id, grant_id, external_ref, kind, data]
      properties:
        id: { $ref: "#/components/schemas/Id" }
        type: { type: string, description: "Open enum, dot-delimited (message.received, …)" }
        occurred_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        api_version: { type: string }
        livemode: { type: boolean }
        business_id: { $ref: "#/components/schemas/Id" }
        installation_id: { $ref: "#/components/schemas/Id" }
        connection_id: { $ref: "#/components/schemas/Id" }
        grant_id: { $ref: "#/components/schemas/Id" }
        external_ref: { type: string }
        kind: { $ref: "#/components/schemas/ConnectionKind" }
        data: { type: object, additionalProperties: true }
      allOf:
        - if:
            required: [type]
            properties:
              type: { enum: [message.delivered, message.read] }
          then:
            properties:
              data: { $ref: "#/components/schemas/MessageStatusData" }
        - if:
            required: [type]
            properties:
              type: { const: message.failed }
          then:
            properties:
              data: { $ref: "#/components/schemas/MessageFailedData" }
        - if:
            required: [type]
            properties:
              type: { const: message.delivery_unknown }
          then:
            properties:
              data: { $ref: "#/components/schemas/MessageDeliveryUnknownData" }
        - if:
            required: [type]
            properties:
              type: { enum: [call.started, call.ringing, call.answered, call.ended] }
          then:
            properties:
              data: { $ref: "#/components/schemas/CallEventData" }
        - if:
            required: [type]
            properties:
              type: { const: call.recording_ready }
          then:
            properties:
              data: { $ref: "#/components/schemas/CallRecordingReadyData" }
        - if:
            required: [type]
            properties:
              type: { const: call.transcript_ready }
          then:
            properties:
              data: { $ref: "#/components/schemas/CallTranscriptReadyData" }
    List:
      type: object
      required: [data, has_more]
      properties:
        data: { type: array, items: {} }
        has_more: { type: boolean }
        next_cursor: { type: [string, "null"] }
  responses:
    Error:
      description: Error
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
paths:
  /api_keys:
    get:
      summary: List API keys for the caller's AppEnvironment
      security: [ { bearer: [] } ]
      x-required-capabilities: [keys:write]
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/StartingAfter" }
      responses:
        "200": { description: OK; secrets are never returned, content: { application/json: { schema: { $ref: "#/components/schemas/ApiKeyList" } } } }
        default: { $ref: "#/components/responses/Error" }
    post:
      summary: Issue an API key (plaintext shown once)
      description: This operation is intentionally not idempotency-replayed. A transport failure must be reconciled by listing keys before another issue attempt.
      security: [ { bearer: [] } ]
      x-required-capabilities: [keys:write]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/CreateApiKeyRequest" } } }
      responses:
        "201": { description: Issued; secret is shown only in this response, content: { application/json: { schema: { $ref: "#/components/schemas/ApiKeyCreatedResponse" } } } }
        default: { $ref: "#/components/responses/Error" }
  /api_keys/{id}/rotate:
    post:
      summary: Rotate an API key and inherit its capabilities
      description: The old key remains valid for at most seven days unless explicitly revoked sooner. This show-once operation is intentionally not idempotency-replayed.
      security: [ { bearer: [] } ]
      x-required-capabilities: [keys:write]
      parameters: [ { name: id, in: path, required: true, schema: { type: string, pattern: '^key_' } } ]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/RotateApiKeyRequest" } } }
      responses:
        "201": { description: Rotated; replacement secret is shown only in this response, content: { application/json: { schema: { $ref: "#/components/schemas/ApiKeyCreatedResponse" } } } }
        default: { $ref: "#/components/responses/Error" }
  /api_keys/{id}:
    delete:
      summary: Revoke an API key immediately
      description: Revocation is immediate, terminal and idempotent; the SDK intentionally does not transport-retry it automatically.
      security: [ { bearer: [] } ]
      x-required-capabilities: [keys:write]
      parameters: [ { name: id, in: path, required: true, schema: { type: string, pattern: '^key_' } } ]
      responses:
        "204": { description: Revoked }
        default: { $ref: "#/components/responses/Error" }
  /installations/{external_ref}:
    put:
      summary: Create or resolve the caller's stable Business installation (upsert)
      parameters:
        - { name: external_ref, in: path, required: true, schema: { type: string, maxLength: 128 } }
        - { $ref: "#/components/parameters/Version" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { business_id: { $ref: "#/components/schemas/Id" } }
      responses:
        "200": { description: Created or existing, content: { application/json: { schema: { $ref: "#/components/schemas/AppInstallation" } } } }
        default: { $ref: "#/components/responses/Error" }
  /installations/{id}/settings:
    patch:
      summary: Change installation send mode or status
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/Version" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              anyOf:
                - required: [send_mode]
                - required: [disabled]
              properties:
                send_mode: { type: string, enum: [human, automated] }
                disabled: { type: boolean }
      responses:
        "200": { description: Updated, content: { application/json: { schema: { $ref: "#/components/schemas/AppInstallation" } } } }
        default: { $ref: "#/components/responses/Error" }
  /connect/sessions:
    post:
      summary: Create a one-use 30-minute hosted-connect session
      security: [ { bearer: [] } ]
      x-required-capabilities: [connections:write]
      parameters:
        - { $ref: "#/components/parameters/Version" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ConnectSessionRequest" } } }
      responses:
        "201": { description: Created; URL contains a one-use opaque launch bearer, content: { application/json: { schema: { $ref: "#/components/schemas/ConnectSessionCreated" } } } }
        default: { $ref: "#/components/responses/Error" }
  /connect/sessions/{id}:
    get:
      summary: Poll an AppEnvironment-owned hosted-connect session
      security: [ { bearer: [] } ]
      x-required-capabilities: [connections:read]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, pattern: '^ses_' } }
        - { $ref: "#/components/parameters/Version" }
      responses:
        "200": { description: Status; provider credentials and internal provisioning state are never returned, content: { application/json: { schema: { $ref: "#/components/schemas/ConnectSession" } } } }
        default: { $ref: "#/components/responses/Error" }
  /connect/s/{token}:
    get:
      summary: Read the connector-owned disclosure without consuming the launch bearer
      security: []
      servers: [ { url: https://api.connectors.lumika.ai } ]
      parameters:
        - { name: token, in: path, required: true, schema: { type: string, pattern: '^cx_launch_[A-Za-z0-9_-]{32,128}$' } }
      responses:
        "200":
          description: Prefetch-safe disclosure; Cache-Control is no-store, private
          headers:
            Cache-Control: { schema: { type: string, const: "no-store, private" } }
            Pragma: { schema: { type: string, const: no-cache } }
          content: { application/json: { schema: { $ref: "#/components/schemas/HostedConnectPreview" } } }
        default: { $ref: "#/components/responses/Error" }
  /connect/s/{token}/start:
    post:
      summary: Explicitly acknowledge connector-owned disclosure and consume the launch bearer
      security: []
      servers: [ { url: https://api.connectors.lumika.ai } ]
      parameters:
        - { name: token, in: path, required: true, schema: { type: string, pattern: '^cx_launch_[A-Za-z0-9_-]{32,128}$' } }
        - { name: Origin, in: header, required: true, schema: { type: string, format: uri }, description: Must exactly equal the connector-owned hosted origin }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/HostedConnectStartRequest" } } }
      responses:
        "200":
          description: Provider redirect or connector-owned onboarding form; Cache-Control is no-store, private
          content: { application/json: { schema: { $ref: "#/components/schemas/HostedConnectStep" } } }
        default: { $ref: "#/components/responses/Error" }
  /connect/s/{token}/complete:
    post:
      summary: Complete a provider-specific hosted onboarding form
      security: []
      servers: [ { url: https://api.connectors.lumika.ai } ]
      parameters:
        - { name: token, in: path, required: true, schema: { type: string, pattern: '^cx_flow_[A-Za-z0-9_-]{32,128}$' } }
      requestBody:
        required: true
        description: Provider-specific connector-hosted fields; credentials are sealed and never echoed.
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200":
          description: Grant committed only after provider subscription succeeds; Cache-Control is no-store, private
          content: { application/json: { schema: { $ref: "#/components/schemas/HostedConnectCompletion" } } }
        default: { $ref: "#/components/responses/Error" }
  /connect/callback/{provider}:
    get:
      summary: Provider authorization-code callback bound to an opaque hosted flow state
      security: []
      servers: [ { url: https://api.connectors.lumika.ai } ]
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string } }
        - { name: state, in: query, required: true, schema: { type: string, pattern: '^cx_flow_[A-Za-z0-9_-]{32,128}$' } }
        - { name: code, in: query, required: true, schema: { type: string } }
      responses:
        "303": { description: Redirect to the exact registered return URL with only session_id }
        default: { $ref: "#/components/responses/Error" }
    post:
      summary: Provider form-post callback bound to an opaque hosted flow state
      security: []
      servers: [ { url: https://api.connectors.lumika.ai } ]
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [state]
              properties:
                state: { type: string, pattern: '^cx_flow_[A-Za-z0-9_-]{32,128}$' }
                code: { type: string }
              additionalProperties: true
      responses:
        "303": { description: Redirect to the exact registered return URL with only session_id }
        default: { $ref: "#/components/responses/Error" }
  /connections:
    get:
      summary: List connections granted to the app
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/StartingAfter" }
        - { name: kind, in: query, schema: { $ref: "#/components/schemas/ConnectionKind" } }
        - { name: status, in: query, schema: { type: string } }
        - { name: external_ref, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/List"
                  - type: object
                    properties: { data: { type: array, items: { $ref: "#/components/schemas/Connection" } } }
        default: { $ref: "#/components/responses/Error" }
  /connections/{id}:
    get:
      summary: One connection
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Connection" } } } }
        default: { $ref: "#/components/responses/Error" }
  /connections/{id}/capabilities:
    get:
      summary: Capabilities for one granted connection
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK; may include an ETag response header, content: { application/json: { schema: { $ref: "#/components/schemas/Capabilities" } } } }
        default: { $ref: "#/components/responses/Error" }
  /connections/{id}/grant:
    delete:
      summary: Give up this app's grant on the connection
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
        - { name: If-Match, in: header, required: false, description: Exact Grant id expected by a tombstone-safe retry, schema: { type: string } }
      responses:
        "204": { description: Revoked }
        default: { $ref: "#/components/responses/Error" }
  /connections/{id}/backfill:
    post:
      summary: Import recent history as historical events
      parameters: [ { name: id, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { threads: { type: integer, minimum: 1, maximum: 100, default: 100 } } } } } }
      responses:
        "202":
          description: Durable history job admitted; replay returns its latest checkpointed status
          headers:
            Idempotent-Replayed:
              description: Present as `true` when the same key replays the durable job status
              schema: { type: string, enum: ["true"] }
          content:
            application/json:
              schema:
                type: object
                required: [job_id, status, accepted, duplicates]
                properties:
                  job_id: { type: string, pattern: '^obx_', description: Opaque durable backfill job id }
                  status: { type: string, enum: [pending, completed, failed] }
                  accepted: { type: integer, minimum: 0 }
                  duplicates: { type: integer, minimum: 0 }
        default: { $ref: "#/components/responses/Error" }
  /conversations:
    get:
      summary: List conversations
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/StartingAfter" }
        - { name: connection_id, in: query, schema: { type: string } }
        - { name: updated_since, in: query, schema: { type: string, format: date-time } }
        - { name: participant_external_id, in: query, schema: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } }
        default: { $ref: "#/components/responses/Error" }
    post:
      summary: Start a business-initiated conversation
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/StartConversationRequest" } } } }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties: { conversation: { $ref: "#/components/schemas/Conversation" }, message: { $ref: "#/components/schemas/Message" } }
        default: { $ref: "#/components/responses/Error" }
  /conversations/{id}:
    get:
      summary: One conversation
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Conversation" } } } }
        default: { $ref: "#/components/responses/Error" }
  /conversations/{id}/messages:
    get:
      summary: Message history (newest first)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: direction, in: query, schema: { type: string, enum: [inbound, outbound] } }
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/StartingAfter" }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } }
        default: { $ref: "#/components/responses/Error" }
    post:
      summary: Send a JSON message
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: "#/components/schemas/SendMessageRequest" } }
      responses:
        "202":
          description: Queued
          content:
            application/json:
              schema: { type: object, properties: { id: { type: string }, client_message_id: { type: string }, status: { type: string, enum: [queued] } } }
        default: { $ref: "#/components/responses/Error" }
  /messages/{id}:
    get:
      summary: Reconcile one message without resending
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/MessageWithAttempts" } } } }
        default: { $ref: "#/components/responses/Error" }
  /messages:
    get:
      summary: Reconcile by caller client_message_id without resending
      parameters: [ { name: client_message_id, in: query, required: true, schema: { type: string, maxLength: 128 } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/MessageWithAttempts" } } } }
        default: { $ref: "#/components/responses/Error" }
  /calls:
    post:
      summary: Durably admit one provider-neutral outbound call
      description: Requires messages:send in both the API key and the caller's exact active Grant. The durable worker rechecks that same Grant and all provider, consent and capability gates immediately before dialing. A transport-ambiguous provider outcome is never dialed again automatically.
      x-required-capabilities: [messages:send]
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: "#/components/schemas/StartCallRequest" } }
      responses:
        "202":
          description: Durable call start queued; the acknowledgement intentionally contains no phone address
          content:
            application/json: { schema: { $ref: "#/components/schemas/StartCallResponse" } }
        default: { $ref: "#/components/responses/Error" }
  /calls/{id}:
    get:
      summary: Reconcile one accessible Call without dialing again
      description: Requires both messages:read and participants:read in the API key and active Grant because a Call contains E.164 endpoints. The current v1 intentionally has no Call list route.
      x-required-capabilities: [messages:read, participants:read]
      parameters: [ { name: id, in: path, required: true, schema: { type: string, pattern: '^cal_[A-Za-z0-9][A-Za-z0-9_-]*$' } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Call" } } } }
        default: { $ref: "#/components/responses/Error" }
  /conversations/{id}/read:
    post:
      summary: Mark read
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { "204": { description: OK }, default: { $ref: "#/components/responses/Error" } }
  /conversations/{id}/typing:
    post:
      summary: Typing indicator
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { on: { type: boolean } } } } } }
      responses: { "204": { description: OK }, default: { $ref: "#/components/responses/Error" } }
  /conversations/{id}/control/acquire:
    post:
      summary: Acquire or renew the enforced controller lease
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ttl_seconds]
              properties:
                ttl_seconds: { type: integer, minimum: 5, maximum: 300 }
                expected_fencing_token: { type: string }
      responses:
        "200":
          description: Acquired or renewed
          content:
            application/json:
              schema:
                type: object
                required: [lease_id, fencing_token, expires_at]
                properties:
                  lease_id: { type: string }
                  fencing_token: { type: string }
                  expires_at: { type: string, format: date-time }
        default: { $ref: "#/components/responses/Error" }
  /conversations/{id}/control/release:
    post:
      summary: Release an owned controller lease
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [lease_id, fencing_token]
              properties: { lease_id: { type: string }, fencing_token: { type: string } }
      responses: { "204": { description: Released }, default: { $ref: "#/components/responses/Error" } }
  /webhook_endpoints:
    get:
      summary: List endpoints
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/StartingAfter" }
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
    post:
      summary: Create an endpoint (secret shown once)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, event_types]
              properties:
                url: { type: string, format: uri }
                event_types:
                  oneOf:
                    - { type: string, enum: ["*"] }
                    - { type: array, minItems: 1, items: { type: string } }
                description: { type: string }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } }
        default: { $ref: "#/components/responses/Error" }
  /webhook_endpoints/{id}:
    get:
      summary: Get one endpoint without its signing secret
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } }
        default: { $ref: "#/components/responses/Error" }
    patch:
      summary: Update URL, subscriptions or status
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                event_types:
                  oneOf:
                    - { type: string, enum: ["*"] }
                    - { type: array, minItems: 1, items: { type: string } }
                description: { type: string, maxLength: 500 }
                status: { type: string, enum: [active, paused] }
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } }, default: { $ref: "#/components/responses/Error" } }
    delete:
      summary: Delete an endpoint
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { "204": { description: Deleted } }
  /webhook_endpoints/{id}/rotate-secret:
    post:
      summary: Rotate an endpoint signing secret
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      responses:
        "201":
          description: Rotated; retries with the same idempotency key recover the original sealed generation after any number of later rotations
          headers:
            Idempotent-Replayed:
              description: Present as `true` when the same key replays the original rotation
              schema: { type: string, enum: ["true"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RotateWebhookSecretResponse" }
        default: { $ref: "#/components/responses/Error" }
  /webhook_endpoints/{id}/test:
    post:
      summary: Queue a deterministic test event for one endpoint
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "202": { description: Queued, content: { application/json: { schema: { $ref: "#/components/schemas/TestWebhookResponse" } } } }
        default: { $ref: "#/components/responses/Error" }
  /webhook_endpoints/{id}/deliveries:
    get:
      summary: List recent delivery attempts for one endpoint
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [pending, claimed, delivered, failed] } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/StartingAfter" }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookDeliveryList" } } } }
        default: { $ref: "#/components/responses/Error" }
  /webhook_endpoints/{id}/recover:
    post:
      summary: Re-queue one bounded page of subscribed events created since a timestamp
      description: Start without cursor, then continue with next_cursor and unchanged since until has_more is false. The initial committed high-water makes the run finite even while new events arrive. A changed endpoint, since, or subscription filter is rejected; an expired cursor returns 410 cursor_expired. Pending and claimed deliveries are never reset.
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [since]
              properties:
                since: { type: string, format: date-time }
                cursor: { $ref: "#/components/schemas/RecoveryCursor" }
                limit: { type: integer, minimum: 1, maximum: 1000, default: 1000 }
      responses:
        "202": { description: Recovery page committed; next_cursor remains non-null when the finite run is exhausted, content: { application/json: { schema: { $ref: "#/components/schemas/RecoverWebhookResponse" } } } }
        "410": { $ref: "#/components/responses/Error" }
        default: { $ref: "#/components/responses/Error" }
  /events:
    get:
      summary: Polling feed of events (same envelopes as webhooks)
      description: Ordered by a durable per-AppEnvironment commit position, not event id or provider time. Always persist next_cursor, even for an empty page. Cursor filters are normalized (types sorted and deduplicated, canonical since) and must remain unchanged. Retained legacy evt_ cursors resolve to their feed position; all responses use evcur_ cursors. Expired or missing legacy cursors return 410 cursor_expired.
      parameters:
        - { name: cursor, in: query, schema: { type: string, maxLength: 1024, description: "Opaque evcur_ checkpoint, or retained legacy evt_ event id" } }
        - { name: types, in: query, schema: { type: string, description: "comma-separated" } }
        - { name: connection_id, in: query, schema: { type: string } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { $ref: "#/components/parameters/Limit" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EventList" }
        "410": { $ref: "#/components/responses/Error" }
        default: { $ref: "#/components/responses/Error" }
  /events/{id}:
    get:
      summary: One accessible event
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Event" } } } }
        default: { $ref: "#/components/responses/Error" }
  /events/{id}/replay:
    post:
      summary: Re-deliver an event to subscribed endpoints
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        "202":
          description: Delivery rows re-queued
          content:
            application/json:
              schema:
                type: object
                required: [event_id, endpoints_queued]
                properties:
                  event_id: { $ref: "#/components/schemas/Id" }
                  endpoints_queued: { type: integer, minimum: 0 }
        default: { $ref: "#/components/responses/Error" }
  /test/connections:
    post:
      summary: Create a deterministic test-environment connection
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateTestConnectionRequest" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Connection" } } } }
        default: { $ref: "#/components/responses/Error" }
  /test/inbound:
    post:
      summary: Sandbox — simulate an inbound message, status, opt-out or call (test connections only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [connection_id, participant_external_id]
              oneOf:
                - required: [text]
                - required: [status]
                - required: [opt_out]
                - required: [call]
              properties:
                connection_id: { type: string }
                participant_external_id: { type: string }
                text: { type: string }
                status: { type: object, additionalProperties: true }
                opt_out: { type: boolean }
                call: { type: object, additionalProperties: true }
      responses:
        "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/TestInboundResponse" } } } }
        default: { $ref: "#/components/responses/Error" }
  /test/provider-failure:
    post:
      summary: Arm the next deterministic test-provider send failure
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TestProviderFailureRequest" }
      responses:
        "204": { description: Failure armed }
        default: { $ref: "#/components/responses/Error" }
