openapi: 3.1.0
info:
  title: Telbox Voice Cloud API
  version: 0.1.1
  description: "The **Telbox Voice Cloud API** exposes Telbox's voice + AI primitives —\ntranscription,\
    \ understanding, ask-anything RAG, translation, and voice\nsynthesis — to third-party applications.\n\
    \n* **Base URL:** `https://api.telbox.ai`\n* **Versioning:** every endpoint is namespaced under `/v1`.\n\
    * **Auth:** `Authorization: Bearer <access_token>` (JWT). Dedicated API keys\n  for server-to-server\
    \ use are on the roadmap; today, obtain a token via the\n  phone-OTP flow.\n\nThis is a curated subset\
    \ of the full Telbox API intended for external\nintegration.\n"
  termsOfService: https://telbox.ai/terms
  contact:
    name: Telbox Developer Support
    email: developers@telbox.ai
  license:
    name: Proprietary
servers:
- url: https://api.telbox.ai
  description: Production
security:
- bearerAuth: []
paths:
  /v1/healthz:
    get:
      tags:
      - health
      summary: Healthz
      description: "Health probe with optional dependency checks.\n\nContract:\n\n* ``GET /healthz?quick=true``\
        \ — returns 200 immediately as long as\n  the FastAPI process is running. Use this for load-balancer\n\
        \  liveness probes; an unhealthy dep should not cause the LB to\n  restart the container.\n\n\
        * ``GET /healthz`` (default) — checks Postgres, Redis, vLLM and\n  the AI worker fleet's liveness.\
        \ Returns 200 if every component\n  is green, 503 if any is down. The body always carries the\n\
        \  per-component verdict so dashboards can graph which dep was\n  degraded.\n\nEach probe has\
        \ its own timeout so a hung dep can't tie up the\nhealth endpoint forever. The probes run concurrently\
        \ — total\nlatency is the slowest single check, not their sum."
      operationId: healthz_v1_healthz_get
      parameters:
      - name: quick
        in: query
        required: false
        schema:
          type: boolean
          description: Skip dependency probes. Used by load-balancer liveness checks that only care if
            the process is alive.
          default: false
          title: Quick
        description: Skip dependency probes. Used by load-balancer liveness checks that only care if the
          process is alive.
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                title: Response Healthz V1 Healthz Get
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security: []
  /v1/auth/phone/start:
    post:
      tags:
      - auth
      summary: Phone Start
      description: 'Begin a phone-OTP sign-in. Either generates a code locally + dispatches

        via OTPSender (log/messages-api), or delegates to Twilio Verify. Rate-limited

        server-side regardless of mode (resend throttle + hourly cap).


        Phase 2.8 — per-phone budget on top of the per-IP global limit.

        SlowAPI''s 10/hour gates by IP, which an attacker rotating IPs

        bypasses; the per-phone token bucket adds a cost the attacker can''t

        rotate around.'
      operationId: phone_start_v1_auth_phone_start_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PhoneStartInput'
        required: true
      responses:
        '202':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneStartOutput'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
      security: []
  /v1/auth/phone/verify:
    post:
      tags:
      - auth
      summary: Phone Verify
      description: 'Verify the OTP, register the device, return tokens — atomic.


        Why one endpoint and not three: the caller has no user_id when it

        starts. Splitting this into separate /verify + /devices/register +

        /sign-in calls would require a temporary "anonymous device" path on

        the device-register endpoint or a one-time bootstrap token. Cleaner

        to do it as a single transaction here.'
      operationId: phone_verify_v1_auth_phone_verify_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PhoneVerifyInput'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneVerifyOutput'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '429':
          $ref: '#/components/responses/RateLimited'
      security: []
  /v1/auth/refresh:
    post:
      tags:
      - auth
      summary: Refresh
      operationId: refresh_v1_auth_refresh_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshInput'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefreshOutput'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security: []
  /v1/me:
    get:
      tags:
      - me
      summary: Me
      operationId: me_v1_me_get
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/threads:
    post:
      tags:
      - threads
      summary: Create Thread
      operationId: create_thread_v1_threads_post
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadCreateInput'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadView'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags:
      - threads
      summary: List Threads
      description: 'List threads. When `workspace_id` is omitted, returns every thread the

        caller participates in across every workspace they belong to.


        Pending invitations (where the caller hasn''t yet accepted) are NOT included;

        fetch those via GET /v1/threads/requests.'
      operationId: list_threads_v1_threads_get
      parameters:
      - name: workspace_id
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Workspace Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ThreadView'
                title: Response List Threads V1 Threads Get
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/threads/{thread_id}:
    get:
      tags:
      - threads
      summary: Get Thread
      description: 'Fetch a single thread by id. Used by the Work tab when the user taps

        a task and we need the destination thread to push into ThreadDetailView.

        Declared *after* the static `/requests` route so FastAPI''s path matcher

        doesn''t try to coerce "requests" into a UUID — that''s how this endpoint

        used to break list_thread_requests.'
      operationId: get_thread_v1_threads__thread_id__get
      parameters:
      - name: thread_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Thread Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadView'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages/send:
    post:
      tags:
      - messages
      summary: Send Message
      operationId: send_message_v1_messages_send_post
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InboundMessage'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResult'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages:
    get:
      tags:
      - messages
      summary: List Messages
      operationId: list_messages_v1_messages_get
      parameters:
      - name: thread_id
        in: query
        required: true
        schema:
          type: string
          format: uuid
          title: Thread Id
      - name: before
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Before
      - name: after
        in: query
        required: false
        schema:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          description: 'Reconnect catch-up: pass the created_at of the newest message the client already
            has, server returns everything newer. Mutually compatible with `before` for paging the tail.'
          title: After
        description: 'Reconnect catch-up: pass the created_at of the newest message the client already
          has, server returns everything newer. Mutually compatible with `before` for paging the tail.'
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 200
          minimum: 1
          default: 50
          title: Limit
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OutboundDelivery'
                title: Response List Messages V1 Messages Get
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages/{message_id}:
    get:
      tags:
      - messages
      summary: Get Message
      description: 'Single-message reconciliation endpoint (Bundle 1).


        Returns the freshest server-side view of one message for the iOS

        foreground / WS-reconnect reconciliation paths. Bearer-auth,

        workspace-scoped (caller must be an active member of the message''s

        thread).


        Caching: emits ``ETag: W/"<digest of updated_at>"``. Clients send

        that back in ``If-None-Match``; we return 304 when it matches.

        Cheap for the inbox-foreground burst case where dozens of bubbles

        are re-checked but most haven''t changed.'
      operationId: get_message_v1_messages__message_id__get
      parameters:
      - name: message_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Message Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: If-None-Match
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: If-None-Match
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageReconciliationView'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages/{message_id}/info:
    get:
      tags:
      - messages
      summary: Message Info
      description: 'Per-message delivery + read details, scoped to the caller''s

        visibility.


        Auth: caller must be the sender (only the sender sees the per-

        recipient delivery breakdown — the recipients see only "read"

        state for their own bubble). Non-senders get 403.'
      operationId: message_info_v1_messages__message_id__info_get
      parameters:
      - name: message_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Message Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageInfoView'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages/{message_id}/transcription:
    patch:
      tags:
      - messages
      summary: Edit Message Transcription
      description: "Edit the auto-generated transcript of an audio message.\n\nAuthorization: only the\
        \ original sender (the speaker) may correct\nthe transcript. Other thread members can request\
        \ a re-transcription\nvia ``POST /messages/{id}/retry-ai``.\n\nBehaviour:\n  1. Load the message\
        \ + verify caller is the sender + the row has\n     an ``ai_payload`` (transcript edits don't\
        \ apply to messages\n     that haven't been transcribed yet).\n  2. Snapshot the prior transcript\
        \ into ``message_edits`` with\n     ``kind='transcription'``.\n  3. Update ``ai_payload.transcript.text``\
        \ and clear any cached\n     translation so the next read re-derives it from the corrected\n \
        \    source.\n  4. Fan out ``message.transcript_edited`` over WS so peer devices\n     splice\
        \ the new text into the existing voice bubble without\n     scrolling."
      operationId: edit_message_transcription_v1_messages__message_id__transcription_patch
      parameters:
      - name: message_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Message Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TranscriptionEditRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageReconciliationView'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/media/upload:
    post:
      tags:
      - media
      summary: Upload Blob
      description: Upload an opaque encrypted blob. Returns the object_key + size.
      operationId: upload_blob_v1_media_upload_post
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  anyOf:
                  - type: string
                  - type: integer
                title: Response Upload Blob V1 Media Upload Post
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/media/{object_key}:
    get:
      tags:
      - media
      summary: Fetch Blob
      description: "Download an encrypted blob.\n\nAuthorization rule (defense in depth):\n    Allow IF\
        \ the caller is a recipient (via `message_recipients`) of any\n    non-deleted message whose `media_object_key`\
        \ matches the requested key.\n\nThis makes cross-workspace voice notes work — Bob in workspace\
        \ B can fetch\na blob whose key embeds workspace A, *iff* Alice (in A) actually addressed\na message\
        \ containing that blob to one of Bob's devices."
      operationId: fetch_blob_v1_media__object_key__get
      parameters:
      - name: object_key
        in: path
        required: true
        schema:
          type: string
          title: Object Key
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/voice-clones:
    get:
      tags:
      - voice_clones
      summary: List Voices
      description: 'All voices the caller can pick from in Settings: built-ins

        first (filtered to the user''s preferred language plus English as

        a universal fallback), then their own clones newest-first.'
      operationId: list_voices_v1_voice_clones_get
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceListResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - voice_clones
      summary: Create Voice Clone
      description: "Accept reference audio, dedupe via Idempotency-Key, enqueue the\ncloning job, return\
        \ 202 with the row's current state.\n\nFlow:\n  1. Validate idempotency-key — must be a UUID.\
        \ Required.\n  2. Look up existing (user_id, idempotency_key) — if hit, return\n     the existing\
        \ row's state verbatim (status from prior call).\n  3. Content-Length pre-check before reading\
        \ body (refuse > 10MB\n     before allocating the memory).\n  4. Validate user + rate-limit (daily\
        \ count + daily bytes +\n     lifetime cap).\n  5. Read body, validate length.\n  6. Stash bytes\
        \ in object storage at workspace-scoped key.\n  7. INSERT the row (catching one-pending-per-user\
        \ collisions).\n  8. Fire the background task. Return 202."
      operationId: create_voice_clone_v1_voice_clones_post
      parameters:
      - name: name
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
      - name: language
        in: query
        required: false
        schema:
          type: string
          default: en
          title: Language
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Idempotency-Key
      - name: Content-Length
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Content-Length
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '202':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceCloneResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/voice-clones/{clone_id}:
    get:
      tags:
      - voice_clones
      summary: Get Voice Clone
      operationId: get_voice_clone_v1_voice_clones__clone_id__get
      parameters:
      - name: clone_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Clone Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceCloneResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/voice-clones/{clone_id}/preview:
    get:
      tags:
      - voice_clones
      summary: Preview Voice Clone
      description: 'Synthesise a short, language-appropriate sample in the user''s

        cloned voice and stream the WAV bytes back. Used by the

        onboarding sheet''s success screen to let the user hear their

        clone before committing.


        Authorization: the clone MUST belong to ``caller.user_id`` AND be

        in ``ready`` status. Returns 404 otherwise so callers can''t probe

        for clone IDs.'
      operationId: preview_voice_clone_v1_voice_clones__clone_id__preview_get
      parameters:
      - name: clone_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Clone Id
      - name: lang
        in: query
        required: false
        schema:
          type: string
          default: en
          title: Lang
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/ask:
    post:
      tags:
      - ask
      summary: Ask
      operationId: ask_v1_ask_post
      parameters:
      - name: on_agent_event
        in: query
        required: false
        schema:
          title: On Agent Event
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/ask/stream:
    post:
      tags:
      - ask
      summary: Ask Stream
      operationId: ask_stream_v1_ask_stream_post
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/ai/ask-by-voice:
    post:
      tags:
      - ask
      summary: Ask By Voice
      description: "Voice → transcript → answer, streamed as SSE.\n\nEvent sequence:\n  1. `data: {\"\
        transcript\": \"...\"}\\n\\n`  (single event; iOS\n     pops the question bubble out of its \"\
        \U0001F3A4 transcribing…\"\n     placeholder using this string).\n  2. `data: {\"text\": \"...\"\
        }\\n\\n` × N  (answer chunks, 30 ms/word).\n  3. `data: {\"citation\": {...}}\\n\\n` × M  (citation\
        \ rows).\n  4. `data: [DONE]\\n\\n`.\n\nError sequence: `data: {\"error\": \"...\"}\\n\\n` then\
        \ `[DONE]`."
      operationId: ask_by_voice_v1_ai_ask_by_voice_post
      parameters:
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskByVoiceRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages/{message_id}/insights:
    post:
      tags:
      - insights
      summary: Generate Insights
      description: 'Run the on-demand AI insights stages (Extract + Summarize + Replies)

        against a message. Streams per-stage progress via the existing

        `ai.stage_done` WS events so the open AIInsightSheet on iOS lights

        up sections one at a time without waiting for the whole batch.'
      operationId: generate_insights_v1_messages__message_id__insights_post
      parameters:
      - name: message_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Message Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsightsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/messages/{message_id}/translate-stream:
    post:
      tags:
      - translate
      summary: Translate Stream
      operationId: translate_stream_v1_messages__message_id__translate_stream_post
      parameters:
      - name: message_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
          title: Message Id
      - name: access_token
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Access Token
      - name: authorization
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Authorization
      - name: x-workspace-id
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Workspace-Id
      requestBody:
        content:
          application/json:
            schema:
              anyOf:
              - $ref: '#/components/schemas/TranslateStreamRequest'
              - type: 'null'
              title: Body
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT access token from `POST /v1/auth/phone/verify` or `/v1/auth/refresh`.
  schemas:
    ThreadAIProcessor:
      properties:
        device_id:
          type: string
          format: uuid
          title: Device Id
        encryption_public_key_b64:
          type: string
          title: Encryption Public Key B64
      type: object
      required:
      - device_id
      - encryption_public_key_b64
      title: ThreadAIProcessor
      description: The AI processor identity for a thread's workspace (public-only).
    EnvelopeWire:
      properties:
        version:
          type: integer
          title: Version
        sender_device_id:
          type: string
          format: uuid
          title: Sender Device Id
        sender_eph_public:
          type: string
          title: Sender Eph Public
        aad:
          type: string
          title: Aad
        ciphertext:
          type: string
          title: Ciphertext
        nonce:
          type: string
          title: Nonce
        signature:
          type: string
          title: Signature
        recipients:
          items:
            $ref: '#/components/schemas/EnvelopeRecipientWire'
          type: array
          title: Recipients
      type: object
      required:
      - version
      - sender_device_id
      - sender_eph_public
      - aad
      - ciphertext
      - nonce
      - signature
      - recipients
      title: EnvelopeWire
      description: 'Inbound envelope from the client over HTTP.


        Bytes are base64url-encoded so the JSON stays compact and URL-safe.'
    MessageReconciliationView:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        thread_id:
          type: string
          format: uuid
          title: Thread Id
        content_type:
          type: string
          title: Content Type
        status:
          type: string
          title: Status
        ai_payload:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          title: Ai Payload
        updated_at:
          type: string
          format: date-time
          title: Updated At
        processing_deadline:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Processing Deadline
        failure_reason:
          anyOf:
          - type: string
          - type: 'null'
          title: Failure Reason
      type: object
      required:
      - id
      - thread_id
      - content_type
      - status
      - ai_payload
      - updated_at
      title: MessageReconciliationView
      description: "Shape returned by GET /v1/messages/{id} for reconciliation.\n\nMinimal by design —\
        \ clients use this ONLY to merge fresh\n``status`` / ``ai_payload`` into an already-decrypted\
        \ local\nbubble. The ciphertext / envelope columns aren't included; the\nclient already has those\
        \ from the original delivery payload.\n\n2026-05-14 additions for the lease state machine:\n \
        \ * ``processing_deadline`` — when the current processing lease\n    expires (None if the row\
        \ isn't currently being processed).\n    Clients use this to stop reconciling after the deadline\
        \ so a\n    stalled server-side pipeline doesn't generate forever-polling\n    on the device.\n\
        \  * ``failure_reason`` — populated only when ``status='failed'``;\n    stable short code (``lease_exhausted``,\
        \ ``stage_failed``, …)\n    the client renders in the bubble's \"what went wrong?\" sheet."
    InsightsResponse:
      properties:
        ai_payload:
          additionalProperties: true
          type: object
          title: Ai Payload
      type: object
      required:
      - ai_payload
      title: InsightsResponse
      description: The full ai_payload after on-demand insights finish.
    TranscriptionEditRequest:
      properties:
        new_transcript:
          type: string
          maxLength: 20000
          minLength: 1
          title: New Transcript
      type: object
      required:
      - new_transcript
      title: TranscriptionEditRequest
      description: 'Body for PATCH /v1/messages/{id}/transcription.


        The caller updates the server-readable transcript text of a voice /

        audio message. The audio bytes stay untouched; only

        ``ai_payload.transcript`` (and the cached translation) change.

        Every edit appends an immutable row to ``message_edits`` for audit

        + ML use; ``thread_call_rag`` and other downstream consumers pick

        up the new text on the next read.'
    Citation:
      properties:
        kind:
          type: string
          title: Kind
        memory_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Memory Id
        message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Message Id
        thread_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Thread Id
        person_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Person Id
        call_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Call Id
        snippet:
          type: string
          title: Snippet
        score:
          anyOf:
          - type: number
          - type: 'null'
          title: Score
      type: object
      required:
      - kind
      - snippet
      title: Citation
    PhoneVerifyOutput:
      properties:
        user_id:
          type: string
          format: uuid
          title: User Id
        device_id:
          type: string
          format: uuid
          title: Device Id
        workspace_id:
          type: string
          format: uuid
          title: Workspace Id
        is_new_user:
          type: boolean
          title: Is New User
        display_name:
          type: string
          title: Display Name
        access_token:
          type: string
          title: Access Token
        refresh_token:
          type: string
          title: Refresh Token
        access_expires_at:
          type: string
          format: date-time
          title: Access Expires At
        refresh_expires_at:
          type: string
          format: date-time
          title: Refresh Expires At
      type: object
      required:
      - user_id
      - device_id
      - workspace_id
      - is_new_user
      - display_name
      - access_token
      - refresh_token
      - access_expires_at
      - refresh_expires_at
      title: PhoneVerifyOutput
    SendMessageResult:
      properties:
        message_id:
          type: string
          format: uuid
          title: Message Id
        server_received_at:
          type: string
          format: date-time
          title: Server Received At
        delivered_to_devices:
          items:
            type: string
            format: uuid
          type: array
          title: Delivered To Devices
        queued_for_devices:
          items:
            type: string
            format: uuid
          type: array
          title: Queued For Devices
      type: object
      required:
      - message_id
      - server_received_at
      - delivered_to_devices
      - queued_for_devices
      title: SendMessageResult
      description: 'Server -> client: confirmation of a delivered message.'
    VoiceItem:
      properties:
        id:
          type: string
          title: Id
        name:
          type: string
          title: Name
        personality:
          anyOf:
          - type: string
          - type: 'null'
          title: Personality
        languages:
          items:
            type: string
          type: array
          title: Languages
          default: []
        gender:
          type: string
          enum:
          - female
          - male
          - neutral
          - unknown
          title: Gender
          default: unknown
        kind:
          type: string
          enum:
          - builtin
          - clone
          title: Kind
          default: builtin
        status:
          type: string
          enum:
          - ready
          - pending
          - processing
          - failed
          title: Status
          default: ready
        created_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Created At
        error_message:
          anyOf:
          - type: string
          - type: 'null'
          title: Error Message
      type: object
      required:
      - id
      - name
      title: VoiceItem
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
    LocationContextItem:
      properties:
        lat:
          type: number
          title: Lat
        lng:
          type: number
          title: Lng
        accuracy_m:
          type: number
          title: Accuracy M
        captured_at:
          type: string
          format: date-time
          title: Captured At
      type: object
      required:
      - lat
      - lng
      - accuracy_m
      - captured_at
      title: LocationContextItem
      description: Phase 8 — single Core Location fix.
    VoiceCloneResponse:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        name:
          type: string
          title: Name
        language:
          type: string
          title: Language
        status:
          type: string
          title: Status
        estimated_seconds:
          anyOf:
          - type: integer
          - type: 'null'
          title: Estimated Seconds
        error_message:
          anyOf:
          - type: string
          - type: 'null'
          title: Error Message
        created_at:
          type: string
          format: date-time
          title: Created At
        ready_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Ready At
      type: object
      required:
      - id
      - name
      - language
      - status
      - created_at
      title: VoiceCloneResponse
    WorkspaceView:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        name:
          type: string
          title: Name
        privacy_tier:
          type: string
          title: Privacy Tier
        ai_processing_mode:
          type: string
          title: Ai Processing Mode
        plan:
          type: string
          title: Plan
        region:
          type: string
          title: Region
        ai_processor:
          anyOf:
          - $ref: '#/components/schemas/AIProcessorInfo'
          - type: 'null'
        echo_thread_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Echo Thread Id
      type: object
      required:
      - id
      - name
      - privacy_tier
      - ai_processing_mode
      - plan
      - region
      title: WorkspaceView
    InboundMessage:
      properties:
        thread_id:
          type: string
          format: uuid
          title: Thread Id
        client_message_id:
          type: string
          maxLength: 64
          minLength: 1
          title: Client Message Id
        content_type:
          type: string
          pattern: ^(text|voice|image|file|location)$
          title: Content Type
        envelope:
          $ref: '#/components/schemas/EnvelopeWire'
        media_object_key:
          anyOf:
          - type: string
          - type: 'null'
          title: Media Object Key
        media_size_bytes:
          anyOf:
          - type: integer
          - type: 'null'
          title: Media Size Bytes
        reply_to_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Reply To Message Id
        forwarded_from_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Forwarded From Message Id
        supersedes_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Supersedes Message Id
        historical_timestamp:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Historical Timestamp
        external_sender_name:
          anyOf:
          - type: string
            maxLength: 120
          - type: 'null'
          title: External Sender Name
      type: object
      required:
      - thread_id
      - client_message_id
      - content_type
      - envelope
      title: InboundMessage
      description: 'Client -> server: post a new message.'
    RefreshInput:
      properties:
        refresh_token:
          type: string
          title: Refresh Token
      type: object
      required:
      - refresh_token
      title: RefreshInput
    MeResponse:
      properties:
        user_id:
          type: string
          format: uuid
          title: User Id
        display_name:
          type: string
          title: Display Name
        email:
          anyOf:
          - type: string
          - type: 'null'
          title: Email
        phone:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone
        workspaces:
          items:
            $ref: '#/components/schemas/WorkspaceView'
          type: array
          title: Workspaces
        devices:
          items:
            $ref: '#/components/schemas/DeviceView'
          type: array
          title: Devices
        avatar_object_key:
          anyOf:
          - type: string
          - type: 'null'
          title: Avatar Object Key
        preferred_language:
          type: string
          title: Preferred Language
          default: en
        transcription_language_pref:
          anyOf:
          - type: string
          - type: 'null'
          title: Transcription Language Pref
        subscription_tier:
          type: string
          title: Subscription Tier
          default: free
        ai_training_consent_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Ai Training Consent At
        ai_processing_consent_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Ai Processing Consent At
        transport_e2e_acknowledged_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Transport E2E Acknowledged At
        workspace_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Workspace Id
        task_extraction_enabled:
          type: boolean
          title: Task Extraction Enabled
          default: true
        ai_feature_quotas:
          anyOf:
          - additionalProperties:
              $ref: '#/components/schemas/AIFeatureQuotaView'
            type: object
          - type: 'null'
          title: Ai Feature Quotas
      type: object
      required:
      - user_id
      - display_name
      - email
      - phone
      - workspaces
      - devices
      title: MeResponse
    EnvelopeRecipientWire:
      properties:
        device_id:
          type: string
          format: uuid
          title: Device Id
        wrapped_key:
          type: string
          title: Wrapped Key
          description: base64url-encoded wrapped session key
      type: object
      required:
      - device_id
      - wrapped_key
      title: EnvelopeRecipientWire
      description: One recipient slot inside an inbound envelope from the client.
    AIProcessorInfo:
      properties:
        device_id:
          type: string
          format: uuid
          title: Device Id
        encryption_public_key_b64:
          type: string
          title: Encryption Public Key B64
        identity_public_key_b64:
          type: string
          title: Identity Public Key B64
      type: object
      required:
      - device_id
      - encryption_public_key_b64
      - identity_public_key_b64
      title: AIProcessorInfo
      description: 'Public-only material for the workspace''s AI processor.


        Clients use this to include the AI processor as a recipient when sealing

        envelopes for Tier 1 workspaces. The encryption_public_key_b64 is the

        X25519 pubkey the wrapped session key gets encrypted to.'
    OutboundDelivery:
      properties:
        type:
          type: string
          title: Type
          default: message.new
        message_id:
          type: string
          format: uuid
          title: Message Id
        thread_id:
          type: string
          format: uuid
          title: Thread Id
        workspace_id:
          type: string
          format: uuid
          title: Workspace Id
        sender_device_id:
          type: string
          format: uuid
          title: Sender Device Id
        sender_user_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Sender User Id
        sender_display_name:
          type: string
          title: Sender Display Name
          default: ''
        content_type:
          type: string
          title: Content Type
        sender_eph_public:
          type: string
          title: Sender Eph Public
        aad:
          type: string
          title: Aad
        ciphertext:
          type: string
          title: Ciphertext
        nonce:
          type: string
          title: Nonce
        signature:
          type: string
          title: Signature
        wrapped_key:
          type: string
          title: Wrapped Key
        server_received_at:
          type: string
          format: date-time
          title: Server Received At
        media_object_key:
          anyOf:
          - type: string
          - type: 'null'
          title: Media Object Key
        total_recipient_devices:
          type: integer
          title: Total Recipient Devices
          default: 0
        delivered_to_devices:
          type: integer
          title: Delivered To Devices
          default: 0
        read_by_devices:
          type: integer
          title: Read By Devices
          default: 0
        ai_payload:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          title: Ai Payload
        status:
          type: string
          title: Status
          default: delivered
        reply_to_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Reply To Message Id
        forwarded_from_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Forwarded From Message Id
        supersedes_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Supersedes Message Id
        reactions:
          items:
            additionalProperties: true
            type: object
          type: array
          title: Reactions
        expires_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Expires At
        external_sender_name:
          anyOf:
          - type: string
          - type: 'null'
          title: External Sender Name
      type: object
      required:
      - message_id
      - thread_id
      - workspace_id
      - sender_device_id
      - sender_user_id
      - content_type
      - sender_eph_public
      - aad
      - ciphertext
      - nonce
      - signature
      - wrapped_key
      - server_received_at
      title: OutboundDelivery
      description: 'Server -> recipient device over WebSocket: a freshly-arrived message.


        The recipient sees the encrypted envelope; the wrapped_key is only the one

        encrypted to *this* device''s public key.'
    PhoneStartOutput:
      properties:
        sent:
          type: boolean
          title: Sent
        expires_at:
          type: string
          format: date-time
          title: Expires At
        dev_code:
          anyOf:
          - type: string
          - type: 'null'
          title: Dev Code
      type: object
      required:
      - sent
      - expires_at
      title: PhoneStartOutput
    TranslateStreamRequest:
      properties:
        target_lang:
          anyOf:
          - type: string
          - type: 'null'
          title: Target Lang
      type: object
      title: TranslateStreamRequest
      description: 'Optional request body for translate-stream.


        The iOS client sends ``target_lang`` to authoritatively pin the

        translation to the user''s *device-local* preferred language —

        this avoids a class of bugs where the user picked a new language

        in Settings, the PATCH /v1/me write failed, and subsequent

        translations came back in the OLD language because the server

        was reading the stale DB value (VD-AI-LANG, 2026-05-09).


        All fields are optional; the server falls through to the

        DB-resolved language when ``target_lang`` is absent.'
    AskByVoiceRequest:
      properties:
        audio_b64:
          type: string
          minLength: 1
          title: Audio B64
        audio_mime:
          type: string
          maxLength: 64
          title: Audio Mime
          default: audio/wav
        hint_language:
          anyOf:
          - type: string
            maxLength: 8
          - type: 'null'
          title: Hint Language
        calendar_events:
          anyOf:
          - items:
              $ref: '#/components/schemas/CalendarContextEvent'
            type: array
          - type: 'null'
          title: Calendar Events
        reminders:
          anyOf:
          - items:
              $ref: '#/components/schemas/ReminderContextItem'
            type: array
          - type: 'null'
          title: Reminders
        contacts:
          anyOf:
          - items:
              $ref: '#/components/schemas/ContactContextItem'
            type: array
          - type: 'null'
          title: Contacts
        location:
          anyOf:
          - $ref: '#/components/schemas/LocationContextItem'
          - type: 'null'
      type: object
      required:
      - audio_b64
      title: AskByVoiceRequest
      description: 'Voice-question body.


        The audio is recorded on-device, base64-encoded, and POSTed here.

        The server transcribes via Gemma-4''s multimodal endpoint (the same

        audio path that powers voice notes), then runs the transcript

        through the same agent pipeline as `/v1/ask/stream`.


        The OS-context bundles (calendar / reminders / contacts / location)

        mirror `AskRequest` so voice asks like "what''s on my calendar

        Thursday?" can reach the Phase 7/8 client-bundle tools. Without

        these the voice path silently runs against empty bundles even when

        the iOS client gathered them.'
    CalendarContextEvent:
      properties:
        title:
          type: string
          maxLength: 300
          title: Title
        start:
          type: string
          format: date-time
          title: Start
        end:
          type: string
          format: date-time
          title: End
        location:
          anyOf:
          - type: string
            maxLength: 300
          - type: 'null'
          title: Location
        calendar_name:
          anyOf:
          - type: string
            maxLength: 120
          - type: 'null'
          title: Calendar Name
        is_all_day:
          type: boolean
          title: Is All Day
          default: false
        url:
          anyOf:
          - type: string
            maxLength: 500
          - type: 'null'
          title: Url
      type: object
      required:
      - title
      - start
      - end
      title: CalendarContextEvent
      description: 'One iOS Calendar event the client surfaces with the request.


        Phase 7a — Calendar data lives in EventKit on iOS; the server

        can''t query it directly. The client reads `EKEventStore` and

        sends a bundle of upcoming events with each Ask request when

        the user has granted Calendar access. The `list_calendar_events`

        tool reads from this bundle.


        Privacy: client only sends events for the next two weeks by

        default + caps at 60 entries. No event UUIDs cross the wire;

        fields are the natural-language facets the agent needs to

        answer "what''s on my calendar Monday."'
    AskResponse:
      properties:
        question:
          type: string
          title: Question
        answer:
          type: string
          title: Answer
        citations:
          items:
            $ref: '#/components/schemas/Citation'
          type: array
          title: Citations
        no_data:
          type: boolean
          title: No Data
          default: false
      type: object
      required:
      - question
      - answer
      - citations
      title: AskResponse
    DeviceView:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        label:
          type: string
          title: Label
        platform:
          type: string
          title: Platform
        created_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Created At
        last_seen_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Last Seen At
        is_current:
          type: boolean
          title: Is Current
          default: false
        encryption_public_key_fingerprint:
          type: string
          title: Encryption Public Key Fingerprint
          default: ''
      type: object
      required:
      - id
      - label
      - platform
      title: DeviceView
    AIFeatureQuotaView:
      properties:
        remaining:
          type: number
          title: Remaining
        capacity:
          type: number
          title: Capacity
        monthly_allowance:
          type: integer
          title: Monthly Allowance
      type: object
      required:
      - remaining
      - capacity
      - monthly_allowance
      title: AIFeatureQuotaView
      description: 'One-feature snapshot of the free-tier monthly quota.


        Emitted on /v1/me only for users on ``subscription_tier=''free''``;

        paid tiers receive ``ai_feature_quotas=None``. iOS uses ``remaining``

        to render the quota meter and decide when to surface the

        "running low — upgrade to Plus" copy.


        The bucket is a continuous token bucket internally, so ``remaining``

        is a float that recovers between calls. ``monthly_allowance`` is the

        sustained per-month refill rate × 2.592e6 seconds — the long-run

        cap a user can use per month. ``capacity`` is the upfront burst the

        bucket allows after a quiet period (catch-up after travel etc.).'
    AskRequest:
      properties:
        question:
          type: string
          maxLength: 400
          minLength: 2
          title: Question
        context_message_id:
          anyOf:
          - type: string
            format: uuid
          - type: 'null'
          title: Context Message Id
        calendar_events:
          anyOf:
          - items:
              $ref: '#/components/schemas/CalendarContextEvent'
            type: array
          - type: 'null'
          title: Calendar Events
        reminders:
          anyOf:
          - items:
              $ref: '#/components/schemas/ReminderContextItem'
            type: array
          - type: 'null'
          title: Reminders
        contacts:
          anyOf:
          - items:
              $ref: '#/components/schemas/ContactContextItem'
            type: array
          - type: 'null'
          title: Contacts
        location:
          anyOf:
          - $ref: '#/components/schemas/LocationContextItem'
          - type: 'null'
      type: object
      required:
      - question
      title: AskRequest
    ContactContextItem:
      properties:
        name:
          type: string
          maxLength: 300
          title: Name
        phones:
          items:
            type: string
          type: array
          maxItems: 10
          title: Phones
        emails:
          items:
            type: string
          type: array
          maxItems: 10
          title: Emails
        org:
          anyOf:
          - type: string
            maxLength: 300
          - type: 'null'
          title: Org
      type: object
      required:
      - name
      title: ContactContextItem
      description: Phase 8 — iOS Contacts bundle entry.
    ThreadCreateInput:
      properties:
        workspace_id:
          type: string
          format: uuid
          title: Workspace Id
        type:
          type: string
          pattern: ^(self|direct|group|imported)$
          title: Type
        member_user_ids:
          items:
            type: string
            format: uuid
          type: array
          title: Member User Ids
        title:
          anyOf:
          - type: string
          - type: 'null'
          title: Title
      type: object
      required:
      - workspace_id
      - type
      title: ThreadCreateInput
      description: 'Client -> server: create a thread.'
    ThreadParticipantView:
      properties:
        user_id:
          type: string
          format: uuid
          title: User Id
        role:
          type: string
          title: Role
          default: member
        accepted_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Accepted At
        joined_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Joined At
        history_visible_after:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: History Visible After
      type: object
      required:
      - user_id
      title: ThreadParticipantView
      description: One active human member in a thread.
    MessageInfoView:
      properties:
        message_id:
          type: string
          format: uuid
          title: Message Id
        server_received_at:
          type: string
          format: date-time
          title: Server Received At
        edited_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Edited At
        deleted_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Deleted At
        total_recipient_devices:
          type: integer
          title: Total Recipient Devices
        delivered_to_devices:
          type: integer
          title: Delivered To Devices
        read_by_devices:
          type: integer
          title: Read By Devices
        deliveries:
          items:
            additionalProperties: true
            type: object
          type: array
          title: Deliveries
      type: object
      required:
      - message_id
      - server_received_at
      - edited_at
      - deleted_at
      - total_recipient_devices
      - delivered_to_devices
      - read_by_devices
      - deliveries
      title: MessageInfoView
      description: 'Phase 9 — per-message delivery + read aggregation.


        The recipient breakdown is computed on the fly from the

        ``message_recipients`` rows the WS / list paths already maintain.

        Returned to the iOS message-info screen so the sender can see

        "delivered to 3 of 4 devices, read by 2".'
    VoiceCloneQuota:
      properties:
        daily_creates_remaining:
          type: integer
          title: Daily Creates Remaining
        daily_bytes_remaining:
          type: integer
          title: Daily Bytes Remaining
        lifetime_creates_remaining:
          type: integer
          title: Lifetime Creates Remaining
      type: object
      required:
      - daily_creates_remaining
      - daily_bytes_remaining
      - lifetime_creates_remaining
      title: VoiceCloneQuota
      description: 'Capacity remaining for the caller — displayed in iOS Settings.


        Numbers reflect the budgets configured in

        :mod:`telbox.modules.abuse`. Surfaced in the GET /voice-clones

        response so the picker can show "3 of 5 today" / "12 of 20 lifetime"

        without a second round-trip.'
    PhoneStartInput:
      properties:
        phone:
          type: string
          title: Phone
          description: E.164 phone number, e.g. +14155551234
        display_name:
          anyOf:
          - type: string
            maxLength: 120
          - type: 'null'
          title: Display Name
          description: Suggested display name; only persisted if this is a new user.
        channel:
          type: string
          enum:
          - sms
          - call
          title: Channel
          default: sms
      type: object
      required:
      - phone
      title: PhoneStartInput
    PhoneVerifyInput:
      properties:
        phone:
          type: string
          title: Phone
        code:
          type: string
          maxLength: 6
          minLength: 6
          title: Code
        device_label:
          type: string
          maxLength: 120
          title: Device Label
        device_platform:
          type: string
          maxLength: 32
          title: Device Platform
          default: ios
        identity_public_key_b64:
          type: string
          maxLength: 128
          minLength: 1
          title: Identity Public Key B64
        encryption_public_key_b64:
          type: string
          maxLength: 128
          minLength: 1
          title: Encryption Public Key B64
        signed_prekey_signature_b64:
          type: string
          maxLength: 128
          minLength: 1
          title: Signed Prekey Signature B64
        voice_identity_pub_b64:
          anyOf:
          - type: string
            maxLength: 128
          - type: 'null'
          title: Voice Identity Pub B64
        user_identity_public_key_b64:
          anyOf:
          - type: string
            maxLength: 128
          - type: 'null'
          title: User Identity Public Key B64
        device_cert_b64:
          anyOf:
          - type: string
            maxLength: 128
          - type: 'null'
          title: Device Cert B64
        wavelink_noise_pub_b64:
          anyOf:
          - type: string
            maxLength: 128
          - type: 'null'
          title: Wavelink Noise Pub B64
      type: object
      required:
      - phone
      - code
      - device_label
      - identity_public_key_b64
      - encryption_public_key_b64
      - signed_prekey_signature_b64
      title: PhoneVerifyInput
      description: 'Atomic phone-OTP bootstrap. The client doesn''t have a device_id

        yet — it has device public keys (just generated locally). We verify

        the OTP, find-or-create the User, register the device, bind tokens —

        all in one call. Solves the chicken-and-egg of pre-registered devices.'
    RefreshOutput:
      properties:
        access_token:
          type: string
          title: Access Token
        refresh_token:
          type: string
          title: Refresh Token
        access_expires_at:
          type: string
          format: date-time
          title: Access Expires At
        refresh_expires_at:
          type: string
          format: date-time
          title: Refresh Expires At
      type: object
      required:
      - access_token
      - refresh_token
      - access_expires_at
      - refresh_expires_at
      title: RefreshOutput
    Error:
      type: object
      title: Error
      properties:
        detail:
          type: string
          description: Machine-readable error code (e.g. `missing_bearer_token`, `ai_quota_exceeded_ask`).
            See the Errors guide.
      required:
      - detail
      example:
        detail: missing_bearer_token
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ReminderContextItem:
      properties:
        title:
          type: string
          maxLength: 300
          title: Title
        list_name:
          anyOf:
          - type: string
            maxLength: 120
          - type: 'null'
          title: List Name
        due:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Due
        is_completed:
          type: boolean
          title: Is Completed
          default: false
        priority:
          type: integer
          title: Priority
          default: 0
        notes:
          anyOf:
          - type: string
            maxLength: 2000
          - type: 'null'
          title: Notes
      type: object
      required:
      - title
      title: ReminderContextItem
      description: 'One iOS Reminders.app item from the client-supplied bundle.


        Phase 7b — same shape as the calendar bundle but for EKReminder

        entities. Read by the `list_reminders` tool.'
    VoiceListResponse:
      properties:
        voices:
          items:
            $ref: '#/components/schemas/VoiceItem'
          type: array
          title: Voices
        selected_voice_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Selected Voice Id
        quota:
          $ref: '#/components/schemas/VoiceCloneQuota'
      type: object
      required:
      - voices
      - quota
      title: VoiceListResponse
    ThreadView:
      properties:
        id:
          type: string
          format: uuid
          title: Id
        workspace_id:
          type: string
          format: uuid
          title: Workspace Id
        type:
          type: string
          title: Type
        title:
          anyOf:
          - type: string
          - type: 'null'
          title: Title
        last_message_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Last Message At
        member_user_ids:
          items:
            type: string
            format: uuid
          type: array
          title: Member User Ids
        participants:
          items:
            $ref: '#/components/schemas/ThreadParticipantView'
          type: array
          title: Participants
        ai_processor:
          anyOf:
          - $ref: '#/components/schemas/ThreadAIProcessor'
          - type: 'null'
        ai_processing_override:
          anyOf:
          - type: string
          - type: 'null'
          title: Ai Processing Override
        last_message_preview:
          anyOf:
          - type: string
          - type: 'null'
          title: Last Message Preview
        last_message_content_type:
          anyOf:
          - type: string
          - type: 'null'
          title: Last Message Content Type
        last_message_duration_seconds:
          anyOf:
          - type: number
          - type: 'null'
          title: Last Message Duration Seconds
        last_message_sent_by_self:
          type: boolean
          title: Last Message Sent By Self
          default: false
        unread_count:
          type: integer
          title: Unread Count
          default: 0
        open_promises_in_thread:
          type: integer
          title: Open Promises In Thread
          default: 0
        disappearing_after_seconds:
          anyOf:
          - type: integer
          - type: 'null'
          title: Disappearing After Seconds
        muted_until:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Muted Until
        peer_subscription_tier:
          anyOf:
          - type: string
          - type: 'null'
          title: Peer Subscription Tier
        peer_ai_enabled:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Peer Ai Enabled
      type: object
      required:
      - id
      - workspace_id
      - type
      - title
      - last_message_at
      - member_user_ids
      title: ThreadView
      description: 'Server -> client: thread summary.'
  responses:
    RateLimited:
      description: Rate limit exceeded. Inspect the `Retry-After` header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, malformed, or expired bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
tags:
- name: auth
  description: Phone-OTP login, token refresh, and legacy email/password.
- name: me
  description: The authenticated user's own profile, devices, and consent.
- name: threads
  description: 'Conversations: create, list, membership, lifecycle.'
- name: messages
  description: Send, fetch, edit, and search messages.
- name: ask
  description: Ask-anything RAG agent over your own history.
- name: insights
  description: On-demand AI insights for a message.
- name: translate
  description: Streaming transcript translation.
- name: voice_clones
  description: 'Voice synthesis: create, list, preview voice clones.'
- name: media
  description: Encrypted blob upload and retrieval.
- name: health
  description: Liveness, readiness, and capability probes.
x-tagGroups:
- name: Identity & Auth
  tags:
  - auth
  - me
- name: Messaging
  tags:
  - threads
  - messages
- name: AI & Insights
  tags:
  - ask
  - insights
  - translate
- name: Voice
  tags:
  - voice_clones
- name: Media
  tags:
  - media
- name: System
  tags:
  - health
