# TextyCally public API spec. `servers[0]` is the production API host so generated SDK
# clients default to https://relay.blackleafdigital.com (matches the @textycally/sdk
# DEFAULT_BASE_URL) — keep it first when updating this file.

openapi: 3.1.0
info:
  title: TextyCally API
  version: 0.1.0
  description: Tenant-facing SMS, voice, and conversation endpoints for TextyCally. Authenticate with your per-client
    bearer API key.
  contact:
    name: TextyCally Support
    email: support@textycally.com
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT
servers:
  - url: https://relay.blackleafdigital.com
    description: TextyCally production API
  - url: http://localhost:3000
    description: Local development
tags:
  - name: notifications
    description: Send and manage notifications.
  - name: conversations
    description: Stateful AI conversations.
  - name: embed
    description: Embeddable widget endpoints.
  - name: web-push
    description: Web Push (VAPID) subscriptions.
  - name: events
    description: Real-time tenant event stream (SSE + WebSocket).
  - name: voice
    description: Voice call analytics — attach agent, read transcript and analysis.
security:
  - bearerApiKey: []
paths:
  /v1/notifications:
    post:
      tags:
        - notifications
      operationId: notifications-create
      summary: Create a notification
      description: |
        Dispatch a notification to one or more recipients across the configured
        channels. Requests are idempotent on `idempotencyKey` scoped to the
        calling client; a repeat key returns the original record with HTTP 200.
        New records return 201.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NotificationRequest"
      responses:
        "200":
          description: Existing notification returned for an idempotent replay.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationResponse"
        "201":
          description: Notification created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
    get:
      tags:
        - notifications
      operationId: notifications-list
      summary: List notifications
      description: List notifications for the authenticated client, newest first. Supports cursor-based pagination.
      parameters:
        - in: query
          name: clientId
          required: false
          schema:
            type: string
            format: uuid
          description: Admin-only filter; ignored for non-admin callers.
        - in: query
          name: status
          required: false
          schema:
            $ref: "#/components/schemas/NotificationStatus"
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - in: query
          name: cursor
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Page of notifications.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationPage"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/notifications/{id}:
    get:
      tags:
        - notifications
      operationId: notifications-get
      summary: Fetch a notification
      description: Return a full notification record including embedded delivery attempts and the attached conversation (if any).
      parameters:
        - $ref: "#/components/parameters/NotificationId"
      responses:
        "200":
          description: Notification found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/notifications/{id}/cancel:
    post:
      tags:
        - notifications
      operationId: notifications-cancel
      summary: Cancel a notification
      description: Cancel a notification that has not yet reached a terminal state. No-op if already terminal.
      parameters:
        - $ref: "#/components/parameters/NotificationId"
      responses:
        "200":
          description: Cancellation accepted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/conversations/{id}:
    get:
      tags:
        - conversations
      operationId: conversations-get
      summary: Fetch a conversation
      description: |
        Return a conversation record including its full transcript. Accessible
        to an admin (Bearer admin or admin session) OR to the owning
        client's API token.
      security:
        - bearerApiKey: []
        - bearerAdmin: []
      parameters:
        - $ref: "#/components/parameters/ConversationId"
      responses:
        "200":
          description: Conversation found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConversationRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/conversations/{id}/messages:
    post:
      tags:
        - conversations
      operationId: conversations-inject-message
      summary: Inject a message into a conversation
      description: |
        Append a message to a conversation transcript. Admin-only. The admin
        auth middleware accepts either an admin Bearer token or a valid
        admin session cookie carrying the `admin` label.
      security:
        - bearerAdmin: []
      parameters:
        - $ref: "#/components/parameters/ConversationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConversationMessageInjectRequest"
      responses:
        "200":
          description: Conversation updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConversationRecord"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/conversations/{id}/confirm:
    post:
      tags:
        - conversations
      operationId: conversations-confirm
      summary: Confirm a conversation via signed token
      description: |
        Used for out-of-band confirmation flows (e.g. an email click-to-confirm
        link). The signed `token` query parameter is the sole credential; no
        bearer or session is required.
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/ConversationId"
        - in: query
          name: token
          required: true
          schema:
            type: string
          description: Signed, time-bound confirmation token.
      responses:
        "200":
          description: Confirmation accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConversationRecord"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/web-push/subscriptions:
    post:
      tags:
        - web-push
      operationId: web-push-subscriptions-create
      summary: Register a Web Push subscription
      description: |
        Client-authed. Upserts a Push Subscription on behalf of the caller's
        end-user. Identity of the subscription is the `(clientId, endpoint)`
        pair — re-posting the same endpoint refreshes `lastUsedAt` and keys.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebPushSubscribeRequest"
      responses:
        "201":
          description: Subscription registered (or refreshed).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebPushSubscribeResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    get:
      tags:
        - web-push
      operationId: web-push-subscriptions-list
      summary: List Web Push subscriptions for a user
      description: |
        Client-authed. Lists active subscriptions for `(clientId, userId)`.
        Deactivated subscriptions are excluded.
      parameters:
        - in: query
          name: userId
          required: true
          schema:
            type: string
            minLength: 1
          description: End-user identifier scoped to the calling client.
      responses:
        "200":
          description: Subscription list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebPushSubscriptionList"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/web-push/subscriptions/{id}:
    delete:
      tags:
        - web-push
      operationId: web-push-subscriptions-delete
      summary: Deactivate a Web Push subscription
      description: |
        Client-authed. Soft-deactivates the subscription so future sends skip
        it. Returns 404 (without leaking existence) if the subscription
        belongs to a different tenant.
      parameters:
        - $ref: "#/components/parameters/WebPushSubscriptionId"
      responses:
        "204":
          description: Subscription deactivated.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/web-push/public-key:
    get:
      tags:
        - web-push
      operationId: web-push-public-key-get
      summary: Fetch the caller's Web Push VAPID public key
      description: |
        Client-authed. Returns the VAPID public key + JWT subject for the
        authenticated client. The browser passes the public key as
        `applicationServerKey` at subscribe time.
      responses:
        "200":
          description: VAPID public key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebPushPublicKeyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/web-push/public-key/{slug}:
    get:
      tags:
        - web-push
      operationId: web-push-public-key-get-by-slug
      summary: Fetch a client's Web Push VAPID public key (unauthenticated)
      description: |
        Unauthenticated lookup keyed by per-client `webpushSlug`. Designed for
        static front-end pages that need the VAPID public key before the user
        is authenticated.
      security:
        - bearerApiKey: []
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
            minLength: 1
          description: Public Web Push slug assigned at key provisioning time.
      responses:
        "200":
          description: VAPID public key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebPushPublicKeyResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/contact:
    post:
      tags:
        - embed
      operationId: embed-contact-submit
      summary: Submit a contact-form message
      description: |
        Backend for the contact-form embed widget. Accepts a
        submitter's name/email/phone plus a free-text message and fans the
        submission out to the tenant's configured channels (intersection of
        `client.allowedChannels` with the optional `channels` filter on the
        body, further filtered down to channels for which a usable address
        was supplied — e.g. `sms` is dropped when no E.164 phone is provided).
        Idempotent within a 1-minute bucket on `(clientId, email, message)`:
        repeat submits inside the same minute return the original record.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedContactRequest"
      responses:
        "200":
          description: Existing notification returned for an idempotent replay.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedContactResponse"
        "202":
          description: Submission accepted and dispatched.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedContactResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/web-push/public-key:
    get:
      tags:
        - embed
      operationId: embed-web-push-public-key
      summary: Fetch the tenant's VAPID public key
      description: |
        Public-key fetch for the push-prompt embed widget. Returns the
        VAPID public key (and subject) provisioned for the tenant identified
        by `:slug`. Idempotent — not rate-limited.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      responses:
        "200":
          description: Public key returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedWebPushPublicKeyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/web-push/subscriptions:
    post:
      tags:
        - embed
      operationId: embed-web-push-subscribe
      summary: Register a Web Push subscription
      description: |
        Upserts a Web Push subscription for the embed-tier widget. Idempotent
        on `(clientId, endpoint)`: re-subscribing the same browser endpoint
        resets `deactivatedAt` and the failure counter and returns the same
        `subscriptionId`. The synthetic `userId` is derived server-side as
        `webpush:<sha256(endpoint)[:32]>` so re-subscribes upsert in place.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedWebPushSubscribeRequest"
      responses:
        "201":
          description: Subscription registered (or upserted).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedWebPushSubscribeResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/web-push/subscriptions/{subscriptionId}:
    delete:
      tags:
        - embed
      operationId: embed-web-push-unsubscribe
      summary: Deactivate a Web Push subscription
      description: |
        Soft-deletes (deactivates) a Web Push subscription. The row is kept
        for audit and skipped by fan-out. Cross-tenant lookups always return
        404 to prevent enumeration of subscription ids across tenants.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - in: path
          name: subscriptionId
          required: true
          schema:
            type: string
            format: uuid
          description: Web Push subscription identifier.
      responses:
        "204":
          description: Subscription deactivated.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/newsletter:
    post:
      tags:
        - embed
      operationId: embed-newsletter-subscribe
      summary: Newsletter signup
      description: |
        Subscribes an email to the tenant's newsletter list. Idempotent on
        `(clientId, email)`: repeat signups return the existing subscriber
        with `alreadySubscribed: true` (HTTP 200) instead of erroring;
        first-time signups return `alreadySubscribed: false` (HTTP 201).
        Never leaks whether the email exists in any other tenant's list.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedNewsletterRequest"
      responses:
        "200":
          description: Email was already on the list — returned for an idempotent replay.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedNewsletterSubscribeResponse"
        "201":
          description: New subscription created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedNewsletterSubscribeResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/newsletter/unsubscribe:
    post:
      tags:
        - embed
      operationId: embed-newsletter-unsubscribe
      summary: Newsletter unsubscribe
      description: |
        Removes an email from the tenant's newsletter list. Always succeeds,
        including for unknown emails — returning 404 here would turn every
        allow-listed origin into a subscriber-enumeration oracle.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedNewsletterUnsubscribeRequest"
      responses:
        "200":
          description: Unsubscribe accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedOkResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/inbox/conversations:
    post:
      tags:
        - embed
      operationId: embed-inbox-bootstrap
      summary: Bootstrap an inbox conversation
      description: |
        Creates a fresh `kind: 'inbox'` conversation for the inbox embed
        widget. Returns the new `conversationId`, an opaque per-conversation
        `token` (returned exactly once — server stores only a SHA-256 hash),
        and the token's `expiresAt`. If `firstMessage` is supplied it is
        seeded into the transcript and the FSM is driven with the
        `GUEST_INITIATED` event.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedInboxBootstrapRequest"
      responses:
        "201":
          description: Inbox conversation created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedInboxBootstrapResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/inbox/conversations/{id}/messages:
    post:
      tags:
        - embed
      operationId: embed-inbox-message-append
      summary: Append a guest message
      description: |
        Appends a guest-authored message to an inbox conversation. The
        `?token=` query parameter is the sole credential — it must match
        the hashed token bound to this conversation at bootstrap time.
        Drives the FSM with `GUEST_MESSAGE` and broadcasts the new
        transcript to every SSE subscriber.
      security:
        - embedConversationToken: []
      parameters:
        - $ref: "#/components/parameters/ConversationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedInboxMessageRequest"
      responses:
        "200":
          description: Message appended.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedInboxMessageResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/inbox/conversations/{id}/transcript:
    get:
      tags:
        - embed
      operationId: embed-inbox-transcript
      summary: Fetch an inbox conversation transcript
      description: |
        Returns the conversation state and full transcript. Used on widget
        reload to rehydrate before resubscribing to the SSE stream. The
        `?token=` query parameter is the sole credential.
      security:
        - embedConversationToken: []
      parameters:
        - $ref: "#/components/parameters/ConversationId"
      responses:
        "200":
          description: Transcript snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedInboxTranscriptResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/inbox/conversations/{id}/stream:
    get:
      tags:
        - embed
      operationId: embed-inbox-stream
      summary: Subscribe to inbox conversation updates (SSE)
      description: |
        Server-Sent Events stream for an inbox conversation. The `?token=`
        query parameter is the sole credential. After authorization the
        connection sends an initial `snapshot` event carrying the current
        transcript shape (`EmbedInboxTranscriptResponse`), followed by
        `transcript-update` events whenever the orchestrator broadcasts a
        new transcript (same payload shape). A 25-second comment-frame
        heartbeat (`:\n\n`) keeps the socket warm under typical
        Cloudflare/nginx idle timeouts.
      security:
        - embedConversationToken: []
      parameters:
        - $ref: "#/components/parameters/ConversationId"
      responses:
        "200":
          description: |
            SSE stream opened. Each event frame is `event: <type>\ndata: <json>\n\n`.
            Event types:
              * `snapshot` — initial transcript snapshot, payload is
                `EmbedInboxTranscriptResponse`.
              * `transcript-update` — new transcript after a state change,
                payload is `EmbedInboxTranscriptResponse`.
              * Heartbeat comment frames (`:\n\n`, every ~25s) are silently
                ignored by browsers.
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events stream. See response description for event types.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/kyc/onboard:
    post:
      tags:
        - embed
      operationId: embed-kyc-onboard
      summary: Account / business onboarding for the calling tenant
      description: |
        Registers a 10DLC business brand + campaign on behalf of the calling
        tenant via the CSP onboarding orchestrator, persists the submission
        (contact / address / tax intake; tax id encrypted at rest). This is
        registration intake, NOT identity verification. Tenant-scoped reflection
        of the operator `POST /v1/admin/onboarding/*` flow — the brand is
        stamped with the tenant's client id. Partial failure is first-class: the
        orchestrator never throws, so a step error is reported in `error` and
        the HTTP status is 200; a fully-successful sequence returns 201. The
        response carries `submissionId` for polling `/kyc/status`. Requires
        Telnyx to be configured (`TELNYX_API_KEY`), else 400.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedKycOnboardRequest"
      responses:
        "200":
          description: Onboarding completed with a captured step error (`error` present).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedKycOnboardResponse"
        "201":
          description: Onboarding completed end-to-end.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedKycOnboardResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/kyc/status:
    get:
      tags:
        - embed
      operationId: embed-kyc-status
      summary: Account / business onboarding status for the calling tenant
      description: |
        Returns the tenant's most-recent persisted onboarding submission
        (status + brand/campaign ids + intake), or `persisted: false` when the
        tenant has never submitted. This is registration status, NOT identity
        verification.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      responses:
        "200":
          description: Onboarding status echo.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedKycStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/numbers:
    get:
      tags:
        - embed
      operationId: embed-numbers-list
      summary: List the calling tenant's phone numbers
      description: |
        Lists the numbers owned by the calling tenant — Telnyx numbers whose
        `customer_reference` equals the client id. Tenant-scoped reflection of
        the operator `GET /v1/admin/numbers`. Numbers tagged to other tenants
        are never returned.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: The tenant's numbers.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedNumbersListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - embed
      operationId: embed-numbers-order
      summary: Search + order a phone number for the calling tenant
      description: |
        Searches for one available number matching the request and orders it,
        stamping `customer_reference` with the tenant's client id so it is
        tenant-tagged from birth. 404 if no number matches.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedNumbersOrderRequest"
      responses:
        "201":
          description: Number ordered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedNumbersOrderResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/numbers/{id}:
    delete:
      tags:
        - embed
      operationId: embed-numbers-release
      summary: Release one of the calling tenant's phone numbers
      description: |
        Releases a number — only if its `customer_reference` matches the
        calling tenant. A number tagged to another tenant returns 403; an
        unknown id returns 404. Prevents releasing numbers a tenant does not
        own by guessing ids.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Telnyx phone-number id.
      responses:
        "204":
          description: Number released.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/porting/check:
    post:
      tags:
        - embed
      operationId: embed-porting-check
      summary: Check number portability for the calling tenant
      description: |
        Checks whether the given E.164 numbers can be ported in (and FastPort
        eligibility) via Telnyx. Tenant-scoped reflection of the operator
        `POST /v1/admin/porting/check`. Does not create an order.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PortabilityCheckRequest"
      responses:
        "200":
          description: Per-number portability results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortabilityCheckResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/UpstreamError"
  /v1/embed/{slug}/porting/orders:
    post:
      tags:
        - embed
      operationId: embed-porting-create
      summary: Create draft porting order(s) for the calling tenant
      description: |
        Creates a Telnyx draft porting order for the given numbers, stamping the
        Telnyx `customer_reference` with the calling client id (the tenant
        boundary) and persisting one `PortingOrder` row per Telnyx order with
        `ownerId` set to the client id. Returns the freshly created orders.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedPortInRequest"
      responses:
        "201":
          description: Draft porting order(s) created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortingOrderCreateResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/UpstreamError"
    get:
      tags:
        - embed
      operationId: embed-porting-list
      summary: List the calling tenant's porting orders
      description: |
        Lists the persisted porting-order rows owned by the calling tenant
        (`ownerId` equals the client id). Orders owned by other tenants are
        never returned. Returns the stored entities (not a live Telnyx
        re-query).
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      responses:
        "200":
          description: The tenant's porting orders.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PortingOrderList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/embed/{slug}/porting/orders/{id}:
    get:
      tags:
        - embed
      operationId: embed-porting-get
      summary: Get / poll one of the calling tenant's porting orders
      description: |
        Re-queries Telnyx for the order's freshest status (own orders only) and
        reflects it onto the stored row; on `ported` finalizes the MSISDN →
        owner assignment. An order owned by another tenant (or unknown) returns
        404 — no cross-tenant existence leak. The `id` is the Telnyx
        porting-order id.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - $ref: "#/components/parameters/PortingOrderId"
      responses:
        "200":
          description: Live porting order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelnyxPortingOrder"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/UpstreamError"
    patch:
      tags:
        - embed
      operationId: embed-porting-update
      summary: Edit / fulfill one of the calling tenant's porting orders
      description: |
        Patches the Telnyx porting order (own orders only) with end-user
        details, FOC activation settings, and attached document ids. Unlike the
        admin surface, `phoneNumberConfiguration` is NOT accepted (operator
        knob). An order owned by another tenant (or unknown) returns 404. The
        `id` is the Telnyx porting-order id.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - $ref: "#/components/parameters/PortingOrderId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedPortInUpdate"
      responses:
        "200":
          description: Updated porting order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelnyxPortingOrder"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/UpstreamError"
  /v1/embed/{slug}/porting/orders/{id}/documents:
    post:
      tags:
        - embed
      operationId: embed-porting-upload-document
      summary: Upload a porting support document (own orders only)
      description: |
        Uploads a supporting document (LOA or invoice) to Telnyx for one of the
        calling tenant's porting orders and returns its document id, referenced
        from the order via `PATCH .../orders/{id}` (`documents.loa` /
        `documents.invoice`). `documentType` defaults to `loa`. An order owned by
        another tenant (or unknown) returns 404.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - $ref: "#/components/parameters/PortingOrderId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/PortingDocumentUploadRequest"
      responses:
        "201":
          description: Document uploaded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadedDocument"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          $ref: "#/components/responses/UpstreamError"
  /v1/embed/{slug}/optin-check:
    post:
      tags:
        - embed
      operationId: embed-optin-check-submit
      summary: Submit an opt-in compliance check
      description: |
        Initiates an opt-in compliance check for the given URL and messaging
        use-case. The server renders the page, extracts opt-in
        signals, and passes the result to an LLM evaluator. The
        pipeline runs synchronously and returns the verdict in the 201 body.

        When a headless renderer or the AI client is not configured on this server, the
        route returns 503 / `failed_precondition` with a human-readable
        message — this is a configuration state, not a transient error.
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmbedOptinCheckRequest"
      responses:
        "201":
          description: Opt-in compliance check completed (or escalated on failure).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmbedOptinCheckResult"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: |
            The opt-in-check agent is not configured on this server (a headless renderer
            or AI client missing). Contact the operator to enable it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FailedPreconditionError"
  /v1/embed/{slug}/optin-check/{id}:
    get:
      tags:
        - embed
      operationId: embed-optin-check-get
      summary: Fetch an opt-in check result
      description: |
        Returns the full opt-in check record by id. The server enforces
        ownership: a tenant may only read its own checks (records belonging
        to a different `clientId` return 404).
      security:
        - publishableKey: []
      parameters:
        - $ref: "#/components/parameters/EmbedSlug"
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Opt-in check id returned by the submit endpoint.
      responses:
        "200":
          description: Opt-in check record.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OptinCheckRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/events/stream:
    get:
      tags:
        - events
      operationId: events-stream
      summary: Tenant real-time event stream (SSE)
      description: |
        Opens a `text/event-stream` connection scoped to the authed client's
        tenant. The server sends an initial `ready` event, then pushes
        `notification.delivery.updated`, `message.received`, and
        `conversation.updated` events as they occur. A `: heartbeat` comment
        frame is sent every 25 seconds to keep the connection alive under
        proxy idle timeouts.

        Reconnect with `Last-Event-ID` is not yet supported — on reconnect
        the client will only see events emitted after the new connection opens.

        **Event shape:**
        ```json
        {
          "type": "notification.delivery.updated" | "message.received" | "conversation.updated",
          "at": "ISO-8601 timestamp",
          "data": { ... }
        }
        ```

        `notification.delivery.updated` data fields:
        - `deliveryId` — the delivery row id.
        - `notificationId` — the parent notification id.
        - `status` — the new delivery status (`sent`, `delivered`, `failed`, …).
        - `channel` — the channel the delivery was attempted on.

        `message.received` data fields:
        - `from` — the sender address (E.164 phone for SMS).
        - `channel` — the inbound channel (e.g. `sms`).
        - `text` — the inbound message body.
        - `conversationId` — the conversation the message was routed to, or `null`.

        `conversation.updated` data fields:
        - `conversationId` — the conversation row id.
        - `state` — the new FSM state.
        - `channel` — the conversation channel.
      security:
        - bearerApiKey: []
      responses:
        "200":
          description: |
            SSE stream opened. Each event is delivered as an SSE `message`
            event whose `data` field is a JSON-encoded `TenantEvent` object.
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events stream (newline-delimited).
        "401":
          $ref: "#/components/responses/Unauthorized"
  /v1/events:
    get:
      tags:
        - events
      operationId: events-socket
      summary: Tenant real-time event stream (WebSocket)
      description: |
        WebSocket upgrade endpoint for the tenant real-time event stream.
        This endpoint MUST be used with a WebSocket upgrade request
        (`Upgrade: websocket`).

        **Authentication** — because browser `WebSocket` cannot set arbitrary
        request headers, the bearer token is accepted via:
        1. `Authorization: Bearer <token>` header (preferred for non-browser clients).
        2. `?token=<bearer>` query parameter (required for browser `WebSocket`).

        On connect the server sends a `ready` JSON text frame:
        ```json
        { "type": "ready", "clientId": "<id>" }
        ```

        Subsequent frames are JSON-encoded `TenantEvent` objects (same shape
        as the SSE endpoint). The connection is server → client only; frames
        sent by the client are silently discarded.

        If a plain HTTP GET is sent (no upgrade), the server returns HTTP 426.
      security:
        - bearerApiKey: []
      parameters:
        - name: token
          in: query
          required: false
          description: |
            Bearer API token (alternative to the `Authorization` header for
            browser WebSocket clients). Exactly one of the header or this
            parameter must be provided.
          schema:
            type: string
      responses:
        "101":
          description: WebSocket upgrade accepted.
        "401":
          description: Missing or invalid bearer token.
        "426":
          description: Upgrade Required — plain HTTP GET on a WebSocket-only endpoint.
  /v1/calls/{id}/agent:
    post:
      tags:
        - voice
      operationId: calls-attach-agent
      summary: Attach voice-analytics agent to a call
      description: |
        Attach the `voice-analytics` agent to an outbound voice call identified
        by its Telnyx callControlId (`{id}`). Creates (or no-ops) the analytics
        row for the delivery so post-call transcript and analysis are captured.
        Ownership is enforced: the resolved call must belong to the authenticated
        client.
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Telnyx callControlId (the providerMessageId on the voice delivery).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agent
              properties:
                agent:
                  type: string
                  enum:
                    - voice-analytics
                  description: Agent to attach. Currently only `voice-analytics` is supported.
      responses:
        "201":
          description: Agent attached successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CallAgentAttachResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/calls/{id}/transcript:
    get:
      tags:
        - voice
      operationId: calls-get-transcript
      summary: Fetch call transcript
      description: |
        Return all utterances captured for a voice call, ordered chronologically.
        Ownership is enforced: the resolved call must belong to the authenticated
        client.
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Telnyx callControlId.
      responses:
        "200":
          description: Transcript returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CallTranscriptResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/calls/{id}/analysis:
    get:
      tags:
        - voice
      operationId: calls-get-analysis
      summary: Fetch post-call analysis
      description: |
        Return the AI-generated post-call analysis for a voice call. Returns
        HTTP 202 with `{ status: "pending" }` when the analytics row does not
        exist yet or the analysis pass has not completed.
        Ownership is enforced: the resolved call must belong to the authenticated
        client.
      security:
        - bearerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Telnyx callControlId.
      responses:
        "200":
          description: Analysis available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CallAnalyticsRecord"
        "202":
          description: Analysis not yet complete.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CallAnalysisPendingResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
components:
  schemas:
    NotificationRequest:
      description: |
        Body of `POST /v1/notifications`. Exactly one of two content modes must
        be supplied: a stored template (`templateId` + `variables`) OR raw
        per-channel payloads (`payloads`).
      allOf:
        - $ref: "#/components/schemas/NotificationRequestBase"
        - oneOf:
            - $ref: "#/components/schemas/NotificationRequestTemplate"
            - $ref: "#/components/schemas/NotificationRequestRaw"
    NotificationResponse:
      description: Full notification view returned by create / get.
      allOf:
        - $ref: "#/components/schemas/NotificationRecord"
        - type: object
          additionalProperties: false
          required:
            - deliveries
            - conversations
          properties:
            deliveries:
              type: array
              items:
                $ref: "#/components/schemas/DeliveryRecord"
            conversations:
              type: array
              description: |
                Conversations bound to this notification — one per matched
                recipient. Empty when no conversation was requested.
              default: []
              items:
                $ref: "#/components/schemas/ConversationRecord"
    NotificationStatus:
      type: string
      description: Lifecycle state of a notification.
      enum:
        - queued
        - in-flight
        - done
        - failed
        - cancelled
    NotificationPage:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/NotificationRecord"
        cursor:
          type: string
          description: Opaque pagination cursor; absent when there are no more pages.
    ConversationRecord:
      type: object
      additionalProperties: false
      required:
        - id
        - notificationId
        - clientId
        - kind
        - state
        - channel
        - transcript
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        notificationId:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        kind:
          $ref: "#/components/schemas/ConversationKind"
        state:
          $ref: "#/components/schemas/ConversationStateName"
        channel:
          $ref: "#/components/schemas/ChannelId"
        transcript:
          type: array
          items:
            $ref: "#/components/schemas/TranscriptEntry"
        artifact:
          type: object
          description: Optional structured artifact (AI draft, chosen option, etc.).
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        finalizedAt:
          type: string
          format: date-time
    ConversationMessageInjectRequest:
      type: object
      additionalProperties: false
      required:
        - role
        - content
      properties:
        role:
          $ref: "#/components/schemas/TranscriptRole"
        content:
          type: string
        providerMessageId:
          type: string
          description: Originating provider message identifier, for de-duplication.
    WebPushSubscribeRequest:
      type: object
      additionalProperties: false
      required:
        - userId
        - subscription
      description: |
        Body of `POST /v1/web-push/subscriptions`. Mirrors the browser's
        `PushSubscription.toJSON()` shape with the addition of a `userId`
        scope.
      properties:
        userId:
          type: string
          minLength: 1
          maxLength: 256
          description: End-user identifier scoped to the calling client.
        subscription:
          type: object
          additionalProperties: false
          required:
            - endpoint
            - keys
          properties:
            endpoint:
              type: string
              format: uri
              description: Push service endpoint URL.
            keys:
              type: object
              additionalProperties: false
              required:
                - p256dh
                - auth
              properties:
                p256dh:
                  type: string
                  minLength: 1
                  description: Subscription P-256 ECDH public key (base64url).
                auth:
                  type: string
                  minLength: 1
                  description: Subscription auth secret (base64url).
        expirationTime:
          type: string
          format: date-time
          description: Optional ISO-8601 expiration as reported by the browser.
        userAgent:
          type: string
          description: Optional user-agent string captured at subscribe time.
    WebPushSubscribeResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        createdAt:
          type: string
          format: date-time
    WebPushSubscriptionList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WebPushSubscription"
    WebPushPublicKeyResponse:
      type: object
      additionalProperties: false
      required:
        - publicKey
        - subject
      properties:
        publicKey:
          type: string
          description: VAPID public key (base64url, uncompressed P-256).
        subject:
          type: string
          description: VAPID JWT `sub` claim (typically `mailto:` or `https://` URL).
    EmbedContactRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - message
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        email:
          type: string
          format: email
          maxLength: 320
        phone:
          type: string
          maxLength: 50
          description: Free-form phone string; E.164 validation is applied separately when fanning to SMS.
        message:
          type: string
          minLength: 1
          maxLength: 5000
        channels:
          type: array
          description: Optional channel subset to fan out to (intersected with `client.allowedChannels`).
          items:
            $ref: "#/components/schemas/EmbedContactChannel"
        metadata:
          type: object
          additionalProperties: true
    EmbedContactResponse:
      type: object
      additionalProperties: false
      required:
        - ok
        - notificationId
        - channels
        - dropped
        - replayed
      properties:
        ok:
          type: boolean
          enum:
            - true
        notificationId:
          type: string
          format: uuid
        channels:
          type: array
          description: Channels the submission was actually dispatched to (intersection of requested + allowed +
            has-usable-address).
          items:
            $ref: "#/components/schemas/ChannelId"
        dropped:
          type: array
          description: Channels that were requested but dropped (e.g. `sms` with no phone).
          items:
            $ref: "#/components/schemas/EmbedContactChannel"
        replayed:
          type: boolean
          description: True when the idempotency-key matched an existing notification within the 1-minute bucket.
    EmbedWebPushPublicKeyResponse:
      type: object
      additionalProperties: false
      required:
        - publicKey
      properties:
        publicKey:
          type: string
          description: VAPID public key (URL-safe base64, uncompressed P-256).
        subject:
          type: string
          description: VAPID subject (mailto/https URL identifying the tenant operator).
    EmbedWebPushSubscribeRequest:
      type: object
      additionalProperties: false
      required:
        - endpoint
        - keys
      properties:
        endpoint:
          type: string
          format: uri
          description: PushSubscription endpoint URL from the browser.
        keys:
          type: object
          additionalProperties: false
          required:
            - p256dh
            - auth
          properties:
            p256dh:
              type: string
              minLength: 1
            auth:
              type: string
              minLength: 1
        expirationTime:
          type: number
          nullable: true
          description: |
            Browser PushSubscription `expirationTime` in epoch milliseconds.
            `null` means "no expiry". The server normalizes to ISO-8601 before
            persisting.
        userAgent:
          type: string
          maxLength: 500
        locale:
          type: string
          maxLength: 20
    EmbedWebPushSubscribeResponse:
      type: object
      additionalProperties: false
      required:
        - subscriptionId
      properties:
        subscriptionId:
          type: string
          format: uuid
          description: Stable subscription row id; reused on idempotent re-subscribes of the same endpoint.
    EmbedNewsletterRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          format: email
          description: Lowercased server-side before persistence.
        source:
          type: string
          maxLength: 128
          description: Optional acquisition source label (e.g. `homepage-footer`).
        metadata:
          type: object
          additionalProperties: true
    EmbedNewsletterSubscribeResponse:
      type: object
      additionalProperties: false
      required:
        - ok
        - subscriberId
        - alreadySubscribed
      properties:
        ok:
          type: boolean
          enum:
            - true
        subscriberId:
          type: string
          format: uuid
        alreadySubscribed:
          type: boolean
          description: True when the email was already on the list (idempotent replay).
    EmbedNewsletterUnsubscribeRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          format: email
    EmbedOkResponse:
      type: object
      additionalProperties: false
      required:
        - ok
      properties:
        ok:
          type: boolean
          enum:
            - true
    EmbedInboxBootstrapRequest:
      type: object
      additionalProperties: false
      properties:
        firstMessage:
          type: string
          minLength: 1
          maxLength: 5000
          description: Optional seed message; appended to the transcript at create time and drives the FSM with `GUEST_INITIATED`.
        metadata:
          type: object
          additionalProperties: true
    EmbedInboxBootstrapResponse:
      type: object
      additionalProperties: false
      required:
        - conversationId
        - inboxSessionId
        - token
        - expiresAt
        - state
      properties:
        conversationId:
          type: string
          format: uuid
        inboxSessionId:
          type: string
          format: uuid
          description: Server-generated session id pinned to this conversation row.
        token:
          type: string
          description: |
            Opaque per-conversation token. Returned exactly once; the server
            persists only a SHA-256 hash. Used as `?token=` for follow-up
            message posts, transcript reads, and the SSE stream.
        expiresAt:
          type: string
          format: date-time
          description: Token expiry (1 hour after creation).
        state:
          $ref: "#/components/schemas/ConversationStateName"
    EmbedInboxMessageRequest:
      type: object
      additionalProperties: false
      required:
        - content
      properties:
        content:
          type: string
          minLength: 1
          maxLength: 5000
    EmbedInboxMessageResponse:
      type: object
      additionalProperties: false
      required:
        - ok
        - state
      properties:
        ok:
          type: boolean
          enum:
            - true
        state:
          $ref: "#/components/schemas/ConversationStateName"
    EmbedInboxTranscriptResponse:
      type: object
      additionalProperties: false
      required:
        - conversationId
        - state
        - transcript
        - updatedAt
      properties:
        conversationId:
          type: string
          format: uuid
        state:
          $ref: "#/components/schemas/ConversationStateName"
        transcript:
          type: array
          items:
            $ref: "#/components/schemas/TranscriptEntry"
        updatedAt:
          type: string
          format: date-time
    EmbedKycOnboardRequest:
      type: object
      additionalProperties: false
      required:
        - brand
        - campaign
      description: |
        Tenant-scoped account / business onboarding. Mirrors
        `CspOnboardingRequest` (minus the `number` step — the owning tenant is
        injected server-side from the publishable key) and adds the
        contact / address / tax intake fields collected by the widget. This is
        registration intake, NOT identity verification.
      properties:
        brand:
          $ref: "#/components/schemas/BrandRequest"
        campaign:
          allOf:
            - $ref: "#/components/schemas/CampaignRequest"
          description: Campaign use-case fields; `brandId` is injected from the new brand.
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
        dob:
          type: string
          description: Date of birth as submitted (`YYYY-MM-DD`); never validated as IDV.
        addressLine1:
          type: string
        addressLine2:
          type: string
        city:
          type: string
        state:
          type: string
        postalCode:
          type: string
        country:
          type: string
          description: ISO-3166-1 alpha-2.
        taxCountry:
          type: string
          description: Tax residency (ISO-3166-1 alpha-2).
        taxId:
          type: string
          description: Plaintext tax id; encrypted at rest server-side. Never returned.
        idType:
          type: string
          description: Submitted government-ID document type label. Not verified.
    EmbedKycOnboardResponse:
      allOf:
        - $ref: "#/components/schemas/CspOnboardingResult"
        - type: object
          properties:
            submissionId:
              type: string
              format: uuid
              description: Id of the persisted submission (poll `/kyc/status`).
      description: Outcome of the tenant onboarding sequence (partial-failure aware).
    EmbedKycStatusResponse:
      oneOf:
        - $ref: "#/components/schemas/EmbedKycStatusEmpty"
        - $ref: "#/components/schemas/EmbedKycStatusPersisted"
      description: |
        Either a `persisted: false` echo (no submission yet) or the tenant's
        most-recent persisted submission.
    EmbedNumbersListResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EmbedNumber"
    EmbedNumbersOrderRequest:
      type: object
      additionalProperties: false
      properties:
        countryCode:
          type: string
          description: ISO-3166-1 alpha-2; defaults to `US`.
        type:
          type: string
          enum:
            - toll_free
            - local
          description: Defaults to `toll_free`.
        features:
          type: array
          items:
            type: string
          description: Telnyx number features; defaults to `['sms', 'voice']`.
        messagingProfileId:
          type: string
          description: Messaging profile to attach at purchase.
    EmbedNumbersOrderResponse:
      type: object
      additionalProperties: false
      required:
        - order
        - phoneNumber
      properties:
        order:
          $ref: "#/components/schemas/NumberOrder"
        phoneNumber:
          type: string
          description: The number that was ordered.
    PortabilityCheckRequest:
      type: object
      additionalProperties: false
      required:
        - phoneNumbers
      properties:
        phoneNumbers:
          type: array
          minItems: 1
          items:
            type: string
            minLength: 1
          description: Non-empty list of E.164 numbers to check.
    PortabilityCheckResponse:
      type: object
      additionalProperties: false
      required:
        - results
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/PortabilityCheckResult"
    EmbedPortInRequest:
      type: object
      additionalProperties: false
      description: |
        Body for creating draft porting order(s) (embed surface). The owner is
        always the calling tenant — the client id is stamped server-side, so no
        `ownerId` is accepted.
      required:
        - phoneNumbers
      properties:
        phoneNumbers:
          type: array
          minItems: 1
          items:
            type: string
            minLength: 1
          description: Non-empty list of E.164 numbers to port in.
    PortingOrderCreateResponse:
      type: object
      additionalProperties: false
      required:
        - orders
      description: |
        The Telnyx orders created from the request (a single request may split
        into several).
      properties:
        orders:
          type: array
          items:
            $ref: "#/components/schemas/TelnyxPortingOrder"
    PortingOrderList:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PortingOrder"
    TelnyxPortingOrder:
      type: object
      additionalProperties: false
      description: |
        Live Telnyx porting order (one of possibly several split from a single
        request) as returned by the create / get / update / confirm / activate
        endpoints.
      required:
        - id
        - status
        - phoneNumbers
      properties:
        id:
          type: string
          description: Telnyx porting-order id.
        status:
          type: string
          description: Raw Telnyx porting-order status string.
        phoneNumbers:
          type: array
          items:
            type: string
          description: E.164 numbers covered by this order.
        customerReference:
          type: string
          description: |
            Telnyx `customer_reference` — the owner tag (client id for embed,
            operator owner reference for admin). Present when set on the order.
        focDatetime:
          type: string
          description: Requested / confirmed FOC datetime (ISO-8601), when present.
        raw:
          description: Raw Telnyx porting-order record.
    EmbedPortInUpdate:
      type: object
      additionalProperties: false
      description: |
        Editable porting-order fields (embed surface). Same as `PortInUpdate`
        minus `phoneNumberConfiguration`, which is an operator-only knob.
      properties:
        endUser:
          $ref: "#/components/schemas/PortInEndUser"
        activationSettings:
          $ref: "#/components/schemas/PortInActivationSettings"
        documents:
          $ref: "#/components/schemas/PortInDocuments"
    PortingDocumentUploadRequest:
      type: object
      description: Multipart upload of a porting support document.
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: The document file (LOA or invoice).
        documentType:
          type: string
          default: loa
          description: |
            Document type tag passed to Telnyx (e.g. `loa`, `invoice`).
            Defaults to `loa` when omitted.
    UploadedDocument:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          description: Telnyx document id, referenced from the order's `documents`.
        raw:
          description: Raw Telnyx document record.
    EmbedOptinCheckRequest:
      type: object
      additionalProperties: false
      required:
        - url
        - useCase
      description: Request body for `POST /v1/embed/{slug}/optin-check`.
      properties:
        url:
          type: string
          format: uri
          description: The URL of the page to check for opt-in language.
        useCase:
          $ref: "#/components/schemas/OptinCheckUseCase"
    EmbedOptinCheckResult:
      type: object
      additionalProperties: false
      required:
        - checkId
        - status
        - confidenceScore
        - verdict
      description: |
        Result of a `POST /v1/embed/{slug}/optin-check` call. Always 201 —
        pipeline failures are represented as `status: escalated` with
        `stepError` populated rather than as HTTP errors.
      properties:
        checkId:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/OptinCheckStatus"
        confidenceScore:
          type: number
          format: float
          nullable: true
          minimum: 0
          maximum: 1
        verdict:
          type: object
          additionalProperties: true
        stepError:
          $ref: "#/components/schemas/OptinCheckStepError"
    FailedPreconditionError:
      type: object
      additionalProperties: false
      required:
        - code
        - message
        - retryable
      description: Returned when a required server-side dependency is not configured.
      properties:
        code:
          type: string
          enum:
            - failed_precondition
        message:
          type: string
        retryable:
          type: boolean
          enum:
            - false
    OptinCheckRecord:
      type: object
      additionalProperties: false
      required:
        - id
        - clientId
        - url
        - useCase
        - status
        - confidenceScore
        - verdict
        - autoApproved
        - humanReviewRequired
        - reviewerId
        - reviewedAt
        - createdAt
        - updatedAt
      description: |
        One row per opt-in check. Created with `status = pending`; the agent
        writes the verdict via `updateVerdict` which resolves status to
        `auto_approved`, `escalated`, or `rejected`. A human reviewer may
        then flip to `manually_approved` via the admin review endpoint.
      properties:
        id:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        url:
          type: string
          format: uri
          description: The URL of the page that was scraped and analysed.
        useCase:
          $ref: "#/components/schemas/OptinCheckUseCase"
        status:
          $ref: "#/components/schemas/OptinCheckStatus"
        confidenceScore:
          type: number
          format: float
          nullable: true
          minimum: 0
          maximum: 1
          description: LLM confidence score. Null until the agent writes the verdict.
        verdict:
          type: object
          additionalProperties: true
          description: |
            Structured LLM verdict (`OptinCheckVerdict`) once populated;
            empty object `{}` until the agent writes the verdict.
        autoApproved:
          type: boolean
          nullable: true
          description: True when the check was automatically approved by the threshold gate.
        humanReviewRequired:
          type: boolean
          description: True when the check requires (or awaited) a human reviewer.
        reviewerId:
          type: string
          nullable: true
          description: Admin label of the reviewer who completed the manual review.
        reviewedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CallAgentAttachResponse:
      type: object
      additionalProperties: false
      required:
        - agentId
        - status
      properties:
        agentId:
          type: string
          example: voice-analytics
        status:
          type: string
          enum:
            - attached
    CallTranscriptResponse:
      type: object
      additionalProperties: false
      required:
        - callId
        - utterances
      properties:
        callId:
          type: string
          description: Telnyx callControlId.
        utterances:
          type: array
          items:
            $ref: "#/components/schemas/CallTranscriptUtterance"
    CallAnalyticsRecord:
      type: object
      additionalProperties: false
      required:
        - id
        - deliveryId
        - notificationId
        - clientId
        - analyticsEnabled
        - consentRecorded
        - consentGranted
        - transcriptionCaptured
        - analysisPayload
        - summary
        - sentiment
        - issues
        - reviewSignals
        - analysisStartedAt
        - analysisCompletedAt
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        deliveryId:
          type: string
          format: uuid
        notificationId:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        analyticsEnabled:
          type: boolean
        consentRecorded:
          type: boolean
        consentGranted:
          type: boolean
          nullable: true
        transcriptionCaptured:
          type: boolean
        analysisPayload:
          type: object
          additionalProperties: true
          description: Raw analysis payload from the AI analysis pass.
        summary:
          type: string
          nullable: true
        sentiment:
          oneOf:
            - $ref: "#/components/schemas/CallSentiment"
            - type: "null"
        issues:
          type: array
          items:
            $ref: "#/components/schemas/CallIssue"
        reviewSignals:
          type: array
          items:
            $ref: "#/components/schemas/CallReviewSignal"
        analysisStartedAt:
          type: string
          format: date-time
          nullable: true
        analysisCompletedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
    CallAnalysisPendingResponse:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - pending
    NotificationRequestBase:
      type: object
      additionalProperties: false
      required:
        - idempotencyKey
        - recipients
      properties:
        idempotencyKey:
          type: string
          minLength: 1
          maxLength: 255
          description: Caller-scoped idempotency key. Identical keys return the original record.
          example: order_42_paid
        recipients:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/NotificationRecipient"
        conversation:
          $ref: "#/components/schemas/NotificationConversationRequest"
        callbackUrl:
          type: string
          format: uri
          description: Optional webhook the relay POSTs to on terminal status.
        bypassQuietHours:
          type: boolean
          description: |
            When `true`, the TCPA quiet-hours gate is bypassed for SMS
            deliveries. Use this for transactional, OTP, or urgent messages
            that must be delivered immediately regardless of the recipient's
            local time. Marketing messages must omit this field (or set it to
            `false`) so quiet-hours enforcement applies. Default: `false`.
    NotificationRequestTemplate:
      type: object
      additionalProperties: false
      required:
        - templateId
      properties:
        templateId:
          type: string
          format: uuid
          description: Identifier of a stored template.
        variables:
          type: object
          description: Variables interpolated into the template.
          additionalProperties: true
    NotificationRequestRaw:
      type: object
      additionalProperties: false
      required:
        - payloads
      properties:
        payloads:
          $ref: "#/components/schemas/ChannelPayloadMap"
    NotificationRecord:
      type: object
      additionalProperties: false
      required:
        - id
        - clientId
        - idempotencyKey
        - rawPayload
        - channels
        - status
        - createdAt
        - updatedAt
        - conversationIds
      properties:
        id:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        idempotencyKey:
          type: string
        templateId:
          type: string
          format: uuid
        rawPayload:
          type: object
          description: Verbatim inbound request body, kept for replay/audit.
          additionalProperties: true
        channels:
          type: array
          items:
            $ref: "#/components/schemas/ChannelId"
        status:
          $ref: "#/components/schemas/NotificationStatus"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        finalizedAt:
          type: string
          format: date-time
        callbackUrl:
          type: string
          format: uri
        conversationIds:
          type: array
          description: |
            Conversation IDs bound to this notification — one per recipient
            whose `address.channel` matched the request's `conversation.channel`.
            Empty when no conversation was requested.
          default: []
          items:
            type: string
            format: uuid
    DeliveryRecord:
      type: object
      additionalProperties: false
      required:
        - id
        - notificationId
        - channel
        - address
        - attempts
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        notificationId:
          type: string
          format: uuid
        channel:
          $ref: "#/components/schemas/ChannelId"
        address:
          $ref: "#/components/schemas/RecipientAddress"
        attempts:
          type: array
          items:
            $ref: "#/components/schemas/DeliveryAttempt"
        status:
          $ref: "#/components/schemas/DeliveryStatus"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        finalizedAt:
          type: string
          format: date-time
    Error:
      type: object
      additionalProperties: false
      required:
        - code
        - message
        - retryable
      properties:
        code:
          type: string
          description: Stable machine-readable error code.
          example: validation_error
        message:
          type: string
          description: Human-readable error message.
          example: "`recipients` must contain at least one entry."
        retryable:
          type: boolean
          description: True when the caller may safely retry the same request.
          example: false
        requestId:
          type: string
          description: Server-assigned correlation identifier for this request.
    ConversationKind:
      type: string
      description: Conversation flow shape.
      enum:
        - approve
        - refine
        - freeform
    ConversationStateName:
      type: string
      description: Lifecycle state of a conversation.
      enum:
        - init
        - prompt-sent
        - awaiting-reply
        - ai-drafting
        - draft-presented
        - awaiting-confirm
        - executing
        - done
        - failed
        - abandoned
    ChannelId:
      type: string
      description: Transport channel identifier.
      enum:
        - sms
        - voice
    TranscriptEntry:
      type: object
      additionalProperties: false
      required:
        - role
        - channel
        - content
        - at
      properties:
        role:
          $ref: "#/components/schemas/TranscriptRole"
        channel:
          $ref: "#/components/schemas/ChannelId"
        content:
          type: string
        at:
          type: string
          format: date-time
    TranscriptRole:
      type: string
      description: Author role for a transcript entry.
      enum:
        - system
        - user
        - assistant
        - action
    WebPushSubscription:
      type: object
      additionalProperties: false
      description: A persisted Web Push subscription owned by a client + end-user.
      required:
        - id
        - userId
        - endpoint
        - p256dh
        - auth
        - createdAt
        - lastUsedAt
        - failureCount
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
        endpoint:
          type: string
          format: uri
        p256dh:
          type: string
        auth:
          type: string
        expirationTime:
          type: string
          format: date-time
        userAgent:
          type: string
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
        lastFailureAt:
          type: string
          format: date-time
        failureCount:
          type: integer
          minimum: 0
        deactivatedAt:
          type: string
          format: date-time
        rotationRequestedAt:
          type: string
          format: date-time
    EmbedContactChannel:
      type: string
      description: Channel subset the contact form is willing to consider.
      enum:
        - email
        - sms
    BrandRequest:
      type: object
      additionalProperties: false
      required:
        - displayName
        - email
        - entityType
        - vertical
      properties:
        country:
          type: string
          description: ISO country code; defaults to `US`.
        displayName:
          type: string
        email:
          type: string
          format: email
        entityType:
          type: string
          description: e.g. `PRIVATE_PROFIT`.
        vertical:
          type: string
          description: e.g. `TECHNOLOGY`.
        companyName:
          type: string
        ein:
          type: string
    CampaignRequest:
      type: object
      additionalProperties: false
      required:
        - brandId
        - description
        - usecase
      properties:
        brandId:
          type: string
        description:
          type: string
        usecase:
          type: string
          description: 10DLC use-case code.
        sample1:
          type: string
        messageFlow:
          type: string
    CspOnboardingResult:
      type: object
      additionalProperties: false
      description: |
        Outcome of the onboarding sequence. Each step that succeeded is present;
        `error` is set when the sequence stopped early, alongside whatever
        already landed.
      properties:
        brand:
          $ref: "#/components/schemas/Brand"
        campaign:
          $ref: "#/components/schemas/Campaign"
        order:
          $ref: "#/components/schemas/NumberOrder"
        assignment:
          $ref: "#/components/schemas/PhoneNumberCampaignAssignment"
        error:
          $ref: "#/components/schemas/CspOnboardingStepError"
    EmbedKycStatusEmpty:
      type: object
      additionalProperties: false
      required:
        - clientId
        - persisted
        - message
      properties:
        clientId:
          type: string
        persisted:
          type: boolean
          enum:
            - false
          description: No onboarding submission for this tenant yet.
        message:
          type: string
    EmbedKycStatusPersisted:
      type: object
      additionalProperties: false
      required:
        - clientId
        - persisted
        - submission
      properties:
        clientId:
          type: string
        persisted:
          type: boolean
          enum:
            - true
        submission:
          $ref: "#/components/schemas/OnboardingSubmission"
    EmbedNumber:
      type: object
      additionalProperties: false
      required:
        - id
        - phoneNumber
      properties:
        id:
          type: string
          description: Telnyx phone-number id.
        phoneNumber:
          type: string
        customerReference:
          type: string
          description: Telnyx `customer_reference` — equals the owning client id.
        raw:
          description: Raw Telnyx number record.
    NumberOrder:
      type: object
      additionalProperties: true
      required:
        - id
        - status
        - phoneNumbers
      properties:
        id:
          type: string
        status:
          type: string
          description: Order status (e.g. `pending`, `success`, `failure`).
        phoneNumbers:
          type: array
          items: {}
        raw:
          description: Raw Telnyx order record.
    PortabilityCheckResult:
      type: object
      additionalProperties: false
      description: Per-number result of a portability check.
      required:
        - phoneNumber
        - portable
        - raw
      properties:
        phoneNumber:
          type: string
          description: The E.164 number checked.
        portable:
          type: boolean
          description: True when the number can be ported in.
        fastPortable:
          type: boolean
          description: True when the number is FastPort-eligible (immediate cutover).
        notPortableReason:
          type: string
          description: Reason the number is not portable, when applicable.
        raw:
          description: Raw Telnyx portability-check row.
    PortingOrder:
      type: object
      additionalProperties: false
      description: |
        Durable persisted port-in order row — TextyCally's tenant-scoped reflection
        of a Telnyx `porting_order` resource. Returned by the list endpoints.
      required:
        - id
        - telnyxOrderId
        - phoneNumber
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Internal porting-order row id.
        telnyxOrderId:
          type: string
          description: |
            Telnyx `porting_order` resource id (UUID). The lifecycle handle for
            edit / confirm / activate / status calls and the webhook correlation key.
        ownerId:
          type: string
          description: |
            Opaque owner reference — the TextyCally client id (embed flow) or an
            operator-supplied owner reference (admin flow). Absent when a draft
            was created before an owner was bound.
        phoneNumber:
          type: string
          description: |
            The primary E.164 number being ported in. A Telnyx order may cover
            several numbers; the full list lives in `raw`.
        status:
          $ref: "#/components/schemas/PortingOrderStatus"
        focDatetime:
          type: string
          description: |
            Requested / confirmed Firm Order Commitment datetime (ISO-8601) — the
            point the losing carrier commits to releasing the number.
        lastComment:
          type: string
          description: |
            Most-recent porting-order comment / exception text, persisted for
            dashboard surfacing.
        raw:
          type: object
          additionalProperties: true
          description: Full raw Telnyx porting-order payload (last seen).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    PortInEndUser:
      type: object
      additionalProperties: false
      properties:
        admin:
          $ref: "#/components/schemas/PortInEndUserAdmin"
        location:
          $ref: "#/components/schemas/PortInEndUserLocation"
    PortInActivationSettings:
      type: object
      additionalProperties: false
      properties:
        focDatetimeRequested:
          type: string
          description: Requested FOC datetime (ISO-8601).
        fastPortEligible:
          type: boolean
          description: Request FastPort (immediate cutover) eligibility.
    PortInDocuments:
      type: object
      additionalProperties: false
      description: |
        Document ids returned by `POST .../orders/{id}/documents` referencing
        the uploaded LOA / invoice.
      properties:
        loa:
          type: string
          description: Document id of the uploaded Letter of Authorization.
        invoice:
          type: string
          description: Document id of the uploaded invoice.
    OptinCheckUseCase:
      type: string
      enum:
        - transactional
        - marketing
        - informational
      description: |
        Messaging use-case that scopes opt-in evidence requirements.
        `marketing` requires the highest confidence; `transactional` the lowest.
    OptinCheckStatus:
      type: string
      enum:
        - pending
        - auto_approved
        - escalated
        - rejected
        - manually_approved
      description: Lifecycle state of the opt-in check.
    OptinCheckStepError:
      type: object
      additionalProperties: false
      required:
        - step
        - message
      properties:
        step:
          type: string
          enum:
            - render
            - extract
            - evaluate
            - threshold
        message:
          type: string
    CallTranscriptUtterance:
      type: object
      additionalProperties: false
      required:
        - id
        - deliveryId
        - notificationId
        - clientId
        - speaker
        - transcript
        - isFinal
        - confidence
        - utteranceStartedAt
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        deliveryId:
          type: string
          format: uuid
        notificationId:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        speaker:
          type: string
          enum:
            - agent
            - user
        transcript:
          type: string
        isFinal:
          type: boolean
        confidence:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
        utteranceStartedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
    CallSentiment:
      type: string
      enum:
        - positive
        - neutral
        - negative
    CallIssue:
      type: object
      additionalProperties: false
      required:
        - type
        - description
        - severity
      properties:
        type:
          type: string
        description:
          type: string
        severity:
          $ref: "#/components/schemas/CallIssueSeverity"
    CallReviewSignal:
      type: object
      additionalProperties: false
      required:
        - signal
        - score
      properties:
        signal:
          type: string
        score:
          type: number
        context:
          type: string
          nullable: true
    NotificationRecipient:
      type: object
      additionalProperties: false
      required:
        - address
      properties:
        address:
          $ref: "#/components/schemas/RecipientAddress"
        payload:
          allOf:
            - $ref: "#/components/schemas/ChannelPayload"
          description: Per-recipient channel overrides; falls back to the top-level `payloads` map when omitted.
    NotificationConversationRequest:
      type: object
      additionalProperties: false
      required:
        - kind
        - channel
      properties:
        kind:
          $ref: "#/components/schemas/ConversationKind"
        channel:
          $ref: "#/components/schemas/ChannelId"
        context:
          type: object
          description: Free-form context handed to the AI / state machine.
          additionalProperties: true
    ChannelPayloadMap:
      type: object
      additionalProperties: false
      description: Per-channel payload map used when not driving a template.
      properties:
        sms:
          $ref: "#/components/schemas/SmsPayload"
        voice:
          $ref: "#/components/schemas/VoicePayload"
    RecipientAddress:
      description: A single recipient address, discriminated on `channel`.
      oneOf:
        - $ref: "#/components/schemas/SmsAddress"
        - $ref: "#/components/schemas/VoiceAddress"
    DeliveryAttempt:
      type: object
      additionalProperties: false
      required:
        - attempt
        - status
        - attemptedAt
      properties:
        attempt:
          type: integer
          minimum: 1
        status:
          $ref: "#/components/schemas/DeliveryStatus"
        providerMessageId:
          type: string
        error:
          type: string
        attemptedAt:
          type: string
          format: date-time
        deliveredAt:
          type: string
          format: date-time
    DeliveryStatus:
      type: string
      description: Lifecycle state of a single per-recipient delivery.
      enum:
        - queued
        - sending
        - sent
        - delivered
        - failed
        - cancelled
    Brand:
      type: object
      additionalProperties: true
      properties:
        brandId:
          type: string
        identityStatus:
          type: string
        status:
          type: string
        raw:
          description: Raw Telnyx 10DLC brand record.
    Campaign:
      type: object
      additionalProperties: true
      properties:
        campaignId:
          type: string
        status:
          type: string
        raw:
          description: Raw Telnyx 10DLC campaign record.
    PhoneNumberCampaignAssignment:
      type: object
      additionalProperties: true
      properties:
        assignmentStatus:
          type: string
        raw:
          description: Raw Telnyx assignment record.
    CspOnboardingStepError:
      type: object
      additionalProperties: false
      required:
        - step
        - message
      properties:
        step:
          type: string
          enum:
            - brand
            - campaign
            - order-number
            - assign-number
          description: Which step failed.
        message:
          type: string
        status:
          type: integer
          description: Upstream HTTP status, when available.
    OnboardingSubmission:
      type: object
      additionalProperties: false
      required:
        - id
        - clientId
        - taxIdConfigured
        - status
        - createdAt
        - updatedAt
      description: |
        A persisted onboarding submission (registration intake, NOT identity
        verification). The submitted tax id is encrypted at rest and never
        returned; `taxIdConfigured` reports whether one was stored.
      properties:
        id:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
        brandId:
          type: string
          description: Telnyx 10DLC brand id.
        campaignId:
          type: string
          description: Telnyx 10DLC campaign id.
        gigsUserId:
          type: string
          description: Gigs MVNO user id
          when provisioned.: null
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
        dob:
          type: string
        addressLine1:
          type: string
        addressLine2:
          type: string
        city:
          type: string
        state:
          type: string
        postalCode:
          type: string
        country:
          type: string
        taxCountry:
          type: string
        taxIdConfigured:
          type: boolean
          description: True when a tax id was submitted and stored encrypted at rest.
        idType:
          type: string
        status:
          type: string
          enum:
            - pending
            - submitted
            - approved
            - rejected
        detail:
          type: string
          description: Free-form operator note (e.g. rejection reason).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    PortingOrderStatus:
      type: string
      enum:
        - draft
        - in-process
        - submitted
        - exception
        - foc-date-confirmed
        - ported
        - cancelled
        - cancel-pending
      description: |
        Telnyx porting-order lifecycle status.
        `draft` → created, not yet filled out / submitted.
        `in-process` → confirmed; Telnyx is working the port.
        `submitted` → submitted to the losing carrier.
        `exception` → action required (rejected / info needed).
        `foc-date-confirmed` → losing carrier confirmed the FOC date.
        `ported` → number is now ours; the MSISDN → owner assignment is finalized.
        `cancelled` → order cancelled.
        `cancel-pending` → cancellation requested, awaiting the carrier.
    PortInEndUserAdmin:
      type: object
      additionalProperties: false
      description: End-user / authorizing-party administrative details.
      properties:
        entityName:
          type: string
        authPersonName:
          type: string
        billingPhoneNumber:
          type: string
        accountNumber:
          type: string
        taxIdentifier:
          type: string
        pinPasscode:
          type: string
          description: The transfer PIN / passcode from the losing carrier.
        businessIdentifier:
          type: string
    PortInEndUserLocation:
      type: object
      additionalProperties: false
      description: End-user service address.
      properties:
        streetAddress:
          type: string
        extendedAddress:
          type: string
        locality:
          type: string
        administrativeArea:
          type: string
        postalCode:
          type: string
        countryCode:
          type: string
    CallIssueSeverity:
      type: string
      enum:
        - low
        - medium
        - high
    ChannelPayload:
      oneOf:
        - $ref: "#/components/schemas/SmsPayload"
        - $ref: "#/components/schemas/VoicePayload"
      discriminator:
        propertyName: channel
        mapping:
          sms: "#/components/schemas/SmsPayload"
          voice: "#/components/schemas/VoicePayload"
    SmsPayload:
      type: object
      additionalProperties: false
      required:
        - channel
      properties:
        channel:
          type: string
          enum:
            - sms
        templateId:
          type: string
          format: uuid
        variables:
          type: object
          description: Free-form variables map.
          additionalProperties: true
        body:
          type: string
        attachments:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/SmsAttachment"
        engine:
          type: string
          enum:
            - telnyx
            - gateway
          description: Explicit per-message SMS engine override (router selection step 1).
        lane:
          type: string
          enum:
            - bulk
            - personalized
            - receive
          description: Compliance/routing lane hint (router selection step 2).
    VoicePayload:
      type: object
      additionalProperties: false
      description: |
        Outbound voice (text-to-speech) payload. The transport places a call and
        speaks `text` once the call is answered.
      required:
        - channel
        - text
      properties:
        channel:
          type: string
          enum:
            - voice
        templateId:
          type: string
          format: uuid
        variables:
          type: object
          description: Free-form variables map.
          additionalProperties: true
        text:
          type: string
          minLength: 1
          description: Text spoken once the call is answered.
        voice:
          type: string
          minLength: 1
          description: Optional named TTS voice (provider-specific, e.g. a Polly voice id).
        payloadType:
          type: string
          enum:
            - text
            - ssml
          description: Whether `text` is plain text or SSML markup. Defaults to `text`.
        language:
          type: string
          description: TTS language/locale selection.
          enum:
            - arb
            - cmn-CN
            - cy-GB
            - da-DK
            - de-DE
            - en-AU
            - en-GB
            - en-GB-WLS
            - en-IN
            - en-US
            - es-ES
            - es-MX
            - es-US
            - fr-CA
            - fr-FR
            - hi-IN
            - is-IS
            - it-IT
            - ja-JP
            - ko-KR
            - nb-NO
            - nl-NL
            - pl-PL
            - pt-BR
            - pt-PT
            - ro-RO
            - ru-RU
            - sv-SE
            - tr-TR
    SmsAddress:
      type: object
      additionalProperties: false
      required:
        - channel
        - phone
      properties:
        channel:
          type: string
          enum:
            - sms
        phone:
          type: string
          description: E.164-formatted phone number.
          pattern: ^\+[1-9]\d{1,14}$
          example: "+14155552671"
    VoiceAddress:
      type: object
      additionalProperties: false
      required:
        - channel
        - phone
      properties:
        channel:
          type: string
          enum:
            - voice
        phone:
          type: string
          description: E.164-formatted phone number to call.
          pattern: ^\+[1-9]\d{1,14}$
          example: "+14155552671"
    SmsAttachment:
      type: object
      additionalProperties: false
      description: |
        MMS attachment for the `sms` channel. The sms-gateway accepts inline
        base64 OR a URL the gateway fetches. Max 1 MB decoded.
      required:
        - mime
      properties:
        filename:
          type: string
          minLength: 1
          description: Optional display name; gateway generates one if absent.
        mime:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/gif
            - video/3gpp
            - audio/amr
        base64:
          type: string
          maxLength: 1398101
          description: |
            Base64-encoded content. Mutually exclusive with `url`. The 1,398,101
            char cap corresponds to ~1 MB decoded (1_048_576 * 4 / 3).
        url:
          type: string
          format: uri
          description: URL the gateway will fetch. Mutually exclusive with `base64`.
        size:
          type: integer
          minimum: 0
          description: Optional decoded size in bytes — informational.
        partId:
          type: string
          minLength: 1
          description: Gateway-side part id; only set on inbound MMS.
  responses:
    ValidationError:
      description: Request body or query failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Authentication failed or was not provided.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Authenticated but not permitted.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Idempotency conflict or resource state conflict.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Gone:
      description: Resource (e.g. a confirmation token) is no longer valid.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    UnprocessableEntity:
      description: Request was syntactically valid but semantically rejected.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Caller exceeded the rate limit or monthly quota.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: Unexpected server-side failure.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    UpstreamError:
      description: An upstream provider (e.g. Telnyx) returned an error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  parameters:
    NotificationId:
      in: path
      name: id
      required: true
      schema:
        type: string
        format: uuid
      description: Notification identifier.
    ConversationId:
      in: path
      name: id
      required: true
      schema:
        type: string
        format: uuid
      description: Conversation identifier.
    WebPushSubscriptionId:
      in: path
      name: id
      required: true
      schema:
        type: string
        format: uuid
      description: Web Push subscription identifier.
    EmbedSlug:
      in: path
      name: slug
      required: true
      schema:
        type: string
        pattern: ^pk_[A-Za-z0-9_-]+$
      description: |
        Tenant publishable key (`pk_*` prefix). Identifies the calling
        client and gates the per-tenant CORS allowlist enforced by
        `embedAuth`.
    PortingOrderId:
      in: path
      name: id
      required: true
      schema:
        type: string
      description: |
        Telnyx porting-order id (UUID). The lifecycle handle for
        edit / confirm / activate / status calls.
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: |
        Per-client opaque API token. Sent as `Authorization: Bearer <token>`.
        Issued once at client creation time and stored hashed server-side.
