openapi: 3.1.0
info:
  title: UpliftAI Voice Agents API
  version: "1.0"
  description: >-
    Build voice assistants, start conversations on the web or the phone, run
    calling campaigns, and receive outcomes by webhook. All endpoints take a
    bearer API key unless noted otherwise.
servers:
  - url: https://api.upliftai.org/v1
security:
  - apiKey: []
tags:
  - name: Assistants
  - name: Authoring an assistant
  - name: Starting a conversation
  - name: Sessions & call records
  - name: Campaigns
  - name: Campaign outcomes
  - name: Contacts
  - name: Google Sheet sources
  - name: Integrations & delivery health
paths:
  /realtime-assistants:
    post:
      operationId: createRealtimeAssistant
      tags:
        - Assistants
      summary: Create an assistant
      description: >-
        Returns the assistant as stored. Provider and model strings are not
        checked here — they resolve when a call starts, so a wrong value fails
        that call, not this request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAssistantRequest'
            examples:
              appointmentConfirmation:
                summary: Appointment confirmation
                value:
                  name: Shifa Clinic appointment confirmation
                  description: Confirms next-day appointments for Shifa Clinic patients
                  public: false
                  config:
                    agent:
                      instructions: "آپ شفا کلینک کی جانب سے کال کر رہے ہیں۔ مریض سے کل کی اپائنٹمنٹ کی تصدیق کریں، وقت بتائیں، اور اگر وہ تاریخ بدلنا چاہیں تو نیا وقت پوچھیں۔ صرف نستعلیق اردو میں جواب دیں۔"
                      initialGreeting: true
                      greetingInstructions: "السلام علیکم کہیں، اپنا اور کلینک کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ بات کر سکتے ہیں۔"
                    session:
                      ttl: 1800
                      roomPrefix: shifa-clinic
                    stt:
                      default:
                        provider: soniox
                        model: stt-rt-preview
                        language: ur
                    tts:
                      default:
                        provider: upliftai
                        voiceId: v_meklc281
                        outputFormat: MP3_22050_32
                    llm:
                      default:
                        provider: google
                        model: gemini-2.5-flash
              withToolEndpoint:
                summary: With a tool that calls your server
                value:
                  name: Order desk
                  description: Answers where-is-my-order calls with a live lookup
                  public: false
                  config:
                    agent:
                      instructions: "آپ ہماری دکان کی جانب سے کال کر رہے ہیں۔ جب کالر آرڈر کے بارے میں پوچھے تو get_order_status سے دیکھیں اور اسٹیٹس اور ڈیلیوری کا دن بتائیں۔ چیک کرنے سے پہلے کہیں کہ آپ سسٹم میں دیکھ رہے ہیں۔ صرف نستعلیق اردو میں جواب دیں۔"
                      initialGreeting: true
                      tools:
                        - name: get_order_status
                          description: Look up an order by its number. Call this when the caller asks where their order is.
                          parameters:
                            type: object
                            properties:
                              orderNumber:
                                type: string
                                description: The order number the caller read out
                            required: [orderNumber]
                          execution:
                            type: tool_endpoint
                            connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    stt:
                      default:
                        provider: soniox
                        model: stt-rt-preview
                        language: ur
                    tts:
                      default:
                        provider: upliftai
                        voiceId: v_meklc281
                        outputFormat: MP3_22050_32
                    llm:
                      default:
                        provider: google
                        model: gemini-2.5-flash
      responses:
        '200':
          description: The created assistant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Assistant'
              example:
                realtimeAssistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic appointment confirmation
                description: Confirms next-day appointments for Shifa Clinic patients
                config:
                  agent:
                    instructions: "آپ شفا کلینک کی جانب سے کال کر رہے ہیں۔ مریض سے کل کی اپائنٹمنٹ کی تصدیق کریں، وقت بتائیں، اور اگر وہ تاریخ بدلنا چاہیں تو نیا وقت پوچھیں۔ صرف نستعلیق اردو میں جواب دیں۔"
                    initialGreeting: true
                    greetingInstructions: "السلام علیکم کہیں، اپنا اور کلینک کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ بات کر سکتے ہیں۔"
                  session:
                    ttl: 1800
                    roomPrefix: shifa-clinic
                  stt:
                    default:
                      provider: soniox
                      model: stt-rt-preview
                      language: ur
                  tts:
                    default:
                      provider: upliftai
                      voiceId: v_meklc281
                      outputFormat: MP3_22050_32
                  llm:
                    default:
                      provider: google
                      model: gemini-2.5-flash
                public: false
                createdAt: '2026-08-18T02:05:22.029Z'
                updatedAt: '2026-08-18T02:05:22.029Z'
                createdBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                updatedBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                version: 1
        '400':
          $ref: '#/components/responses/ConfigBadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    get:
      operationId: listRealtimeAssistants
      tags:
        - Assistants
      summary: List assistants
      description: >-
        Every assistant in the API key's project, as its draft. `aliases` on
        each row says whether it has been published and whether the draft is
        ahead of what calls run.
      responses:
        '200':
          description: Every assistant in the project.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AssistantListItem'
              example:
                - realtimeAssistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  name: Edversity admissions follow-up
                  description: Calls warm leads about the cybersecurity programme
                  config:
                    agent:
                      instructions: "آپ زارا ہیں، ایڈورسٹی کی ایڈمیشن کونسلر۔ سائبر سیکیورٹی پروگرام میں دلچسپی لینے والے لیڈز کو کال کریں، فیس اور کلاس شیڈول بتائیں، اور ڈیمو سیشن کے لیے وقت طے کریں۔"
                      initialGreeting: true
                      greetingInstructions: "اپنا اور ایڈورسٹی کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ ابھی بات کر سکتے ہیں۔"
                      tools: []
                    session:
                      ttl: 1800
                    stt:
                      default:
                        provider: soniox
                        model: stt-rt-v4
                        language: ur
                    tts:
                      default:
                        provider: upliftai
                        voiceId: v_8eelc901
                        outputFormat: MP3_22050_32
                    llm:
                      default:
                        provider: openai
                        model: gpt-5.4-mini
                        reasoningEffort: none
                    noiseCancellation:
                      enabled: false
                  public: false
                  createdAt: '2026-08-18T09:41:10.517Z'
                  updatedAt: '2026-08-18T10:02:47.883Z'
                  createdBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  version: 14
                  aliases:
                    draft:
                      version: 14
                      updatedAt: '2026-08-18T10:02:47.883Z'
                      updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    prod:
                      version: 11
                      updatedAt: '2026-08-18T09:58:03.120Z'
                      updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                - realtimeAssistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  name: Shifa Clinic appointment confirmation
                  description: Confirms next-day appointments for Shifa Clinic patients
                  config:
                    agent:
                      instructions: "آپ شفا کلینک کی جانب سے کال کر رہے ہیں۔ مریض سے کل کی اپائنٹمنٹ کی تصدیق کریں، وقت بتائیں، اور اگر وہ تاریخ بدلنا چاہیں تو نیا وقت پوچھیں۔ صرف نستعلیق اردو میں جواب دیں۔"
                      initialGreeting: true
                      greetingInstructions: "السلام علیکم کہیں، اپنا اور کلینک کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ بات کر سکتے ہیں۔"
                    session:
                      ttl: 1800
                      roomPrefix: shifa-clinic
                    stt:
                      default:
                        provider: soniox
                        model: stt-rt-preview
                        language: ur
                    tts:
                      default:
                        provider: upliftai
                        voiceId: v_meklc281
                        outputFormat: MP3_22050_32
                    llm:
                      default:
                        provider: google
                        model: gemini-2.5-flash
                  public: false
                  createdAt: '2026-08-18T02:05:22.029Z'
                  updatedAt: '2026-08-18T02:05:22.029Z'
                  createdBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  updatedBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  version: 1
                  aliases:
                    draft:
                      version: 1
                      updatedAt: '2026-08-18T02:05:22.029Z'
                      updatedBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /realtime-assistants/{realtimeAssistantId}:
    get:
      operationId: getRealtimeAssistant
      tags:
        - Assistants
      summary: Get an assistant
      description: >-
        Config comes back verbatim — omitted fields stay omitted — which makes
        this the read step before an update, since updates replace rather than
        merge. You get the draft unless you ask for a `version`.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 26932cc0-88a1-4f06-b7d2-4be0a3f0d571
        - name: version
          in: query
          required: false
          description: >-
            `draft`, `prod`, or a published version number. `prod` and a number
            return exactly what a call on them would run. A number that was
            never published is a 404.
          schema:
            oneOf:
              - type: string
                enum: [draft, prod]
              - type: integer
                minimum: 1
            default: draft
          example: prod
      responses:
        '200':
          description: The assistant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Assistant'
              example:
                realtimeAssistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic appointment confirmation
                description: Confirms next-day appointments for Shifa Clinic patients
                config:
                  agent:
                    instructions: "آپ شفا کلینک کی جانب سے کال کر رہے ہیں۔ مریض سے کل کی اپائنٹمنٹ کی تصدیق کریں، وقت بتائیں، اور اگر وہ تاریخ بدلنا چاہیں تو نیا وقت پوچھیں۔ صرف نستعلیق اردو میں جواب دیں۔"
                    initialGreeting: true
                    greetingInstructions: "السلام علیکم کہیں، اپنا اور کلینک کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ بات کر سکتے ہیں۔"
                  session:
                    ttl: 1800
                    roomPrefix: shifa-clinic
                  stt:
                    default:
                      provider: soniox
                      model: stt-rt-preview
                      language: ur
                  tts:
                    default:
                      provider: upliftai
                      voiceId: v_meklc281
                      outputFormat: MP3_22050_32
                  llm:
                    default:
                      provider: google
                      model: gemini-2.5-flash
                public: false
                createdAt: '2026-08-18T02:05:22.029Z'
                updatedAt: '2026-08-18T02:05:22.029Z'
                createdBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                updatedBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                version: 1
        '400':
          description: '`version` is not `draft`, `prod` or a whole number above 0.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "version must be 'prod', 'draft' or a published version number"
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such assistant in the API key's project, or no such published version of it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unknownAssistant:
                  summary: No assistant with that id
                  value:
                    message: 'Realtime assistant not found: 00000000-0000-4000-8000-000000000000'
                    error: Not Found
                    statusCode: 404
                neverPublished:
                  summary: A version number that was never published
                  value:
                    message: Version 7 of realtime assistant 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx was never published
                    error: Not Found
                    statusCode: 404
        '503':
          $ref: '#/components/responses/VersionNotReady'
    post:
      operationId: updateRealtimeAssistant
      tags:
        - Assistants
      summary: Update an assistant
      description: >-
        This replaces rather than merges, so read the assistant, change what you
        need, and send the whole object back. Leaving out `description` or
        `public` clears them — `public` resets to false — while `name` and
        `config` keep their stored values. Only populated parts of `config` are
        validated, so a config missing `agent.instructions` saves fine and
        leaves the assistant unable to take calls.


        Until the assistant's first publish, a save goes live at once. After
        that, an update changes the draft only. Calls and share links keep
        running the published version, and the `public` check reads it too,
        until you [publish](/api-reference/assistants/publish-an-assistant-version)
        again.


        `noiseCancellation` and `backgroundNoise` are checked on every save that
        sends `config`. An assistant stored with a bad value before these checks
        gets a 400 on that save until you fix it. A save without `config` still
        goes through.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 80b00435-4f7d-4555-b7c2-778229512f0b
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAssistantRequest'
            example:
              name: Al-Falah credit card payment reminder
              description: Reminds cardholders of the due date and offers to log a payment promise
              public: false
              config:
                agent:
                  instructions: "آپ الفلاح بینک کی جانب سے کال کر رہے ہیں۔ کارڈ ہولڈر کو ادائیگی کی آخری تاریخ یاد دلائیں، واجب الادا رقم بتائیں، اور اگر وہ ادائیگی کا وعدہ کریں تو تاریخ نوٹ کریں۔ صرف نستعلیق اردو میں جواب دیں۔"
                  initialGreeting: true
                  greetingInstructions: "السلام علیکم کہیں، بینک کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ ابھی بات کر سکتے ہیں۔"
                session:
                  ttl: 1800
                  roomPrefix: al-falah
                stt:
                  default:
                    provider: soniox
                    model: stt-rt-preview
                    language: ur
                tts:
                  default:
                    provider: upliftai
                    voiceId: v_meklc281
                    outputFormat: MP3_22050_32
                llm:
                  default:
                    provider: google
                    model: gemini-2.5-flash
      responses:
        '200':
          description: The assistant as stored after the update.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Assistant'
              example:
                realtimeAssistantId: 80b00435-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Al-Falah credit card payment reminder
                description: Reminds cardholders of the due date and offers to log a payment promise
                config:
                  agent:
                    instructions: "آپ الفلاح بینک کی جانب سے کال کر رہے ہیں۔ کارڈ ہولڈر کو ادائیگی کی آخری تاریخ یاد دلائیں، واجب الادا رقم بتائیں، اور اگر وہ ادائیگی کا وعدہ کریں تو تاریخ نوٹ کریں۔ صرف نستعلیق اردو میں جواب دیں۔"
                    initialGreeting: true
                    greetingInstructions: "السلام علیکم کہیں، بینک کا تعارف کروائیں، پھر پوچھیں کہ کیا وہ ابھی بات کر سکتے ہیں۔"
                  session:
                    ttl: 1800
                    roomPrefix: al-falah
                  stt:
                    default:
                      provider: soniox
                      model: stt-rt-preview
                      language: ur
                  tts:
                    default:
                      provider: upliftai
                      voiceId: v_meklc281
                      outputFormat: MP3_22050_32
                  llm:
                    default:
                      provider: google
                      model: gemini-2.5-flash
                public: false
                createdAt: '2026-08-18T04:37:53.727Z'
                updatedAt: '2026-08-18T04:39:28.828Z'
                createdBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                updatedBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                version: 6
        '400':
          $ref: '#/components/responses/ConfigBadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteRealtimeAssistant
      tags:
        - Assistants
      summary: Delete an assistant
      description: >-
        Deleting is quiet: you get a 204 even if that id never existed, so a
        success is not proof anything was there. Every published version goes
        with it. Campaigns keep the now-dangling `assistantId`, and a running
        one pauses itself at its next dial with `statusMessage: "Paused:
        assistant version not found"`. The assistant's session list stops
        resolving, so pull the call history before you delete.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: cc66dfda-2af8-4f4a-9f5f-a56adfa151de
      responses:
        '204':
          description: Empty body.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/AssistantBusy'
  /realtime-assistants/{realtimeAssistantId}/publish:
    post:
      operationId: publishRealtimeAssistant
      tags:
        - Assistants
      summary: Publish an assistant version
      description: >-
        Choose the version calls run. `version: "draft"` publishes your saved
        edits. A number points calls back at a version you published before,
        which is how you roll back. Neither touches the draft. Until the first
        publish every save goes live at once. After it, saves change the draft
        only, so publish again to put them on calls. Publishing what `prod`
        already runs changes nothing and still returns 200. See
        [Versions](/voice-agents/assistants/versions).
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublishAssistantRequest'
            examples:
              firstPublish:
                summary: Put the saved draft on calls
                value:
                  version: draft
                  message: Fee schedule for the October intake
              rollback:
                summary: Point calls back at an earlier version
                value:
                  version: 11
      responses:
        '200':
          description: Where `draft` and `prod` point now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantPublishState'
              examples:
                firstPublish:
                  summary: The draft is now what calls run
                  value:
                    realtimeAssistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Edversity admissions follow-up
                    description: Calls warm leads about the cybersecurity programme
                    aliases:
                      draft:
                        version: 14
                        updatedAt: '2026-08-18T10:02:47.883Z'
                        updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      prod:
                        version: 14
                        updatedAt: '2026-08-18T10:05:12.406Z'
                        updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                rollback:
                  summary: Calls run version 11 again, the draft stays at 14
                  value:
                    realtimeAssistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Edversity admissions follow-up
                    description: Calls warm leads about the cybersecurity programme
                    aliases:
                      draft:
                        version: 14
                        updatedAt: '2026-08-18T10:02:47.883Z'
                        updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      prod:
                        version: 11
                        updatedAt: '2026-08-18T11:40:09.018Z'
                        updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '400':
          description: '`version` is missing, `prod`, or a number sent as a string. Nothing was published.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                publishProd:
                  summary: '`prod` is where you publish to, not something to publish'
                  value:
                    message:
                      - "version must be 'draft' or a published version number"
                    error: Bad Request
                    statusCode: 400
                quotedNumber:
                  summary: 'The version sent as "11" instead of 11'
                  value:
                    message:
                      - "version must be 'draft' or a published version number"
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such assistant in the API key's project, or no such published version to roll back to.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unknownAssistant:
                  summary: No assistant with that id
                  value:
                    message: 'Realtime assistant not found: 00000000-0000-4000-8000-000000000000'
                    error: Not Found
                    statusCode: 404
                neverPublished:
                  summary: Rolling back to a number that was never published
                  value:
                    message: Version 9 of realtime assistant 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx was never published
                    error: Not Found
                    statusCode: 404
        '409':
          description: >-
            Nothing was published. Read the assistant again if a save landed,
            then retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                beingModified:
                  summary: A save, build or other publish holds the assistant
                  value:
                    message: The realtime assistant is being modified, try again shortly
                    error: Conflict
                    statusCode: 409
                changedMidPublish:
                  summary: A save or another publish landed while this one ran
                  value:
                    message: The realtime assistant changed while it was being published
                    error: Conflict
                    statusCode: 409
  /realtime-assistants/{realtimeAssistantId}/versions:
    get:
      operationId: listAssistantVersions
      tags:
        - Assistants
      summary: List published versions
      description: >-
        Every version you published, newest first, without configs. Read one
        in full with [Get an assistant](/api-reference/assistants/get-an-assistant)
        and `?version=`. `aliases` marks the one calls run. Empty until the
        first publish.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: limit
          in: query
          required: false
          description: Versions per page. Values out of range are clamped, not rejected.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          required: false
          description: The previous page's `nextCursor`, passed back unchanged. A mangled one is a 400.
          schema:
            type: string
      responses:
        '200':
          description: One page of published versions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantVersionList'
              example:
                versions:
                  - version: 14
                    name: Edversity admissions follow-up
                    description: Calls warm leads about the cybersecurity programme
                    message: Fee schedule for the October intake
                    publishedAt: '2026-08-18T10:05:12.406Z'
                    publishedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  - version: 11
                    name: Edversity admissions follow-up
                    description: Calls warm leads about the cybersecurity programme
                    message: Shorter greeting, demo slot offered first
                    publishedAt: '2026-08-18T09:58:03.120Z'
                    publishedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                aliases:
                  draft:
                    version: 14
                    updatedAt: '2026-08-18T10:02:47.883Z'
                    updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  prod:
                    version: 11
                    updatedAt: '2026-08-18T11:40:09.018Z'
                    updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '400':
          description: The `cursor` could not be read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /realtime-assistants/{realtimeAssistantId}/metadata:
    get:
      operationId: getAssistantPublishState
      tags:
        - Assistants
      summary: Get an assistant's publish state
      description: >-
        Where `draft` and `prod` point, without any config. `prod` is absent
        until the first publish. When `draft.version` is ahead of
        `prod.version`, the draft has edits that calls don't run yet.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The assistant's aliases.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantPublishState'
              examples:
                unpublishedEdits:
                  summary: Published at 11, draft saved since
                  value:
                    realtimeAssistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Edversity admissions follow-up
                    description: Calls warm leads about the cybersecurity programme
                    aliases:
                      draft:
                        version: 14
                        updatedAt: '2026-08-18T10:02:47.883Z'
                        updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      prod:
                        version: 11
                        updatedAt: '2026-08-18T09:58:03.120Z'
                        updatedBy: d9164fbd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                neverPublished:
                  summary: Never published, so every save is live
                  value:
                    realtimeAssistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Shifa Clinic appointment confirmation
                    description: Confirms next-day appointments for Shifa Clinic patients
                    aliases:
                      draft:
                        version: 1
                        updatedAt: '2026-08-18T02:05:22.029Z'
                        updatedBy: ef80cc74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /realtime-assistants/builds:
    post:
      operationId: startAssistantBuild
      tags:
        - Authoring an assistant
      summary: Build an assistant from a brief
      description: >-
        The assistant exists the moment this returns, but a background pipeline
        writes and hardens its Urdu prompt over the next few minutes — until the
        status poll reads `done`, its entire prompt is the literal text
        `(building…)`, so don't put it on a call yet. [Poll the build
        status](/api-reference/authoring-an-assistant/poll-a-builds-progress)
        for the outcome. Every
        request builds a fresh assistant; to change one you already built,
        [refine it](/api-reference/authoring-an-assistant/refine-a-built-assistants-prompt)
        or [update its config
        directly](/api-reference/assistants/update-an-assistant).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartBuildRequest'
            example:
              brief:
                goal: appointment
                voiceId: v_meklc281
                gender: female
                context: >-
                  Shifa Clinic, Lahore. Call patients to confirm tomorrow's
                  doctor appointment, remind them of the time, and offer to
                  reschedule if they cannot come.
                sampleRows:
                  - name: عائشہ صدیقی
                    phone: '+923001234567'
                    doctor: ڈاکٹر عمران شیخ
                    appointmentTime: کل صبح 11 بجے
      responses:
        '200':
          description: >-
            The build is running; its outcome arrives through the status poll.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartBuildResponse'
              example:
                buildId: 01407eda-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                assistantId: 9d1d042f-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                status: started
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - 'brief.goal must be one of the following values: appointment, event, lead, order_confirmation, payment_reminder, outreach, reservation_confirmation'
                  - brief.voiceId must be a string
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /realtime-assistants/builds/status/{buildId}:
    get:
      operationId: getAssistantBuildStatus
      tags:
        - Authoring an assistant
      summary: Poll a build's progress
      description: >-
        Works for refine runs too — poll whichever `buildId` you got back. Runs
        take minutes, so poll every few seconds. `done` only means the pipeline
        stopped; whether the grader was satisfied is `result.passed`. Live
        detail (`phase`, `round`, transcript counts) is kept for an hour after
        the run last moved; after that the poll still returns the outcome —
        `result` on success, `error` on failure.
      parameters:
        - name: buildId
          in: path
          required: true
          description: The `buildId` the build or refine returned.
          schema:
            type: string
            format: uuid
          example: 425d3b75-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The run's latest frame.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuildProgress'
              examples:
                inProgress:
                  summary: Rehearsing the first round of test calls
                  value:
                    buildId: 425d3b75-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    status: running
                    round: 1
                    phase: testing
                    persona: Ayesha Siddiqi — Lahore patient confirming tomorrow 11am, keeps asking about fees
                    transcriptsDone: 7
                    transcriptsTotal: 10
                failed:
                  summary: A failed run — the reason outlives the hour of live detail
                  value:
                    buildId: 425d3b75-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    assistantId: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    status: failed
                    phase: failed
                    round: 0
                    error: 'Refine failed: LLM provider rejected the request'
                done:
                  summary: Finished after three rounds, findings still open
                  value:
                    buildId: 425d3b75-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    assistantId: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    status: done
                    phase: done
                    round: 0
                    result:
                      assistantId: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      promptVersion: 3
                      passed: false
                      chosen:
                        happy:
                          label: "عائشہ صدیقی — cheerful appointment confirmation for Dr. عمران شیخ, Lahore"
                          archetype: happy
                          path: happy
                          turns:
                            - speaker: agent
                              text: |-
                                السلام علیکم، کیا میں عائشہ صدیقی سے بات کر رہی ہوں؟

                                میں Shifa Clinic سے ثناء بات کر رہی ہوں۔ کل کی appointment confirm کرنی تھی، ایک منٹ بات کر سکتی ہوں؟
                            - speaker: caller
                              text: "وعلیکم السلام بھئی جی، جی میں عائشہ صدیقی ہی بول رہی ہوں۔ ہاں جی بالکل، آپ ایک منٹ بات کر لیں۔"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No run with that id in this project — including a run that died before it could write an outcome.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'No build found for 3f2a9c14-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
                error: Not Found
                statusCode: 404
  /realtime-assistants/builds/{assistantId}/simulate:
    post:
      operationId: simulateAssistantBuild
      tags:
        - Authoring an assistant
      summary: Rehearse a call against a persona
      description: |-
        Text-only: no call is placed, and it runs whatever prompt the assistant has saved right now, on the assistant's own LLM provider, model, and generation settings. Despite the `builds` path, it works on any assistant — built or hand-written.

        Three inputs aim the simulation:

        - `persona` — who answers. `behavior` goes verbatim into the caller model's system prompt; `path` defaults to `negative`, because the uncooperative arc is where prompts break.
        - `context` — the scene: what the caller already knows about who is calling and why. Omit it to fall back to the build brief's context; with neither, the caller answers like a genuine cold call.
        - `variables` — the customer record for this one call, same as `variables` on a real outbound call. Both sides see it: the agent substitutes its tokens, the caller answers consistently with it.

        Anything wrong with the setup — an unsupported provider, a bad body — fails as a plain JSON 400 before any streaming starts. Seen enough mid-conversation? Just disconnect: the run stops server-side, and so does the spend.
      parameters:
        - name: assistantId
          in: path
          required: true
          description: The assistant to rehearse.
          schema:
            type: string
            format: uuid
          example: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulateRequest'
            example:
              persona:
                label: بے صبرا گاہک
                behavior: >-
                  Interrupts early, demands to know who is calling and where
                  they got the number, and hangs up if the answer is vague.
                path: negative
              context: >-
                Khwab Clinic, Lahore. You booked a dream-interpretation session
                last week and the clinic is calling back to walk you through the
                dream you submitted.
              variables:
                name: عائشہ صدیقی
                city: لاہور
                dream: پانی میں چلنے کا خواب
      responses:
        '200':
          description: |-
            An event stream, not a JSON body. It looks like this on the wire:

            ```text
            event: turn
            data: {"index":1,"role":"agent","text":"السلام علیکم، میں شفا کلینک سے ثناء بات کر رہی ہوں۔"}

            : ping

            event: turn
            data: {"index":2,"role":"caller","text":"جی، کون؟ آپ کو میرا نمبر کہاں سے ملا؟"}

            event: done
            data: {"personaId":"b8bed320-…","personaLabel":"بے صبرا گاہک","archetype":"happy","path":"negative","turns":[…]}
            ```

            - `turn` carries `{index, role, text}` — `index` is 1-based speaking order, `role` is `agent` or `caller`.
            - Exactly one terminal event ends the stream: `done` with the whole transcript, or `error` with a `{message}` — a conversation that broke mid-flight, or one that hit the five-minute ceiling.
            - Either side can end the conversation, and the caller often does — don't assume the transcript ends on the agent's closing line.
            - On `done`, `personaId` is minted for this run and `archetype` is always `happy`, whatever `path` you sent.
            - `: ping` keepalives arrive every 15 seconds; SSE parsers drop them.
            - A stream that ends with neither terminal event is a dropped connection — there is no resume, so run it again.
          content:
            text/event-stream:
              schema:
                type: string
              example: |
                event: turn
                data: {"index":1,"role":"agent","text":"السلام علیکم، میں خواب کلینک سے ڈاکٹر عامرہ بات کر رہی ہوں۔"}

                event: turn
                data: {"index":2,"role":"caller","text":"جی، کون؟ آپ کو میرا نمبر کہاں سے ملا؟"}

                event: done
                data: {"personaId":"b8bed320-xxxx-xxxx-xxxx-xxxxxxxxxxxx","personaLabel":"بے صبرا گاہک","archetype":"happy","path":"negative","turns":[{"speaker":"agent","text":"السلام علیکم، میں خواب کلینک سے ڈاکٹر عامرہ بات کر رہی ہوں۔"},{"speaker":"caller","text":"جی، کون؟ آپ کو میرا نمبر کہاں سے ملا؟"}]}
        '400':
          description: Validation failed, or the assistant's LLM cannot be simulated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                validation:
                  summary: The request body was rejected
                  value:
                    message:
                      - persona.behavior should not be empty
                      - 'persona.path must be one of the following values: happy, negative'
                    error: Bad Request
                    statusCode: 400
                unsupportedProvider:
                  summary: The assistant's LLM provider is not supported
                  value:
                    message: Simulation does not support LLM provider 'anthropic'
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /realtime-assistants/builds/{assistantId}/refine:
    post:
      operationId: refineAssistantBuild
      tags:
        - Authoring an assistant
      summary: Refine a built assistant's prompt
      description: >-
        A finished refine regenerates the assistant's `name` and its whole
        `config` from the brief, overwriting any edits you made by hand — only
        the prompt carries forward. It reruns the same rehearse-and-grade rounds
        as a build, so it takes minutes: [poll the build
        status](/api-reference/authoring-an-assistant/poll-a-builds-progress).
        Starting one while another build is in flight still returns 200, then
        surfaces as `failed` in that poll.
      parameters:
        - name: assistantId
          in: path
          required: true
          description: The `assistantId` the original build returned.
          schema:
            type: string
            format: uuid
          example: befbca99-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefineAssistantRequest'
            example:
              ownerFeedback: "اپائنٹمنٹ کا وقت اور ڈاکٹر کا نام دو بار دہرائیں۔ اگر مریض کہے کہ وہ ابھی مصروف ہیں تو بحث نہ کریں، معذرت کر کے کال ختم کر دیں۔"
      responses:
        '200':
          description: >-
            The refine started; nothing here reflects its outcome. A run that cannot start — because a build is already in flight for this assistant — still returns 200, and surfaces as `failed` in the status poll.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartBuildResponse'
              example:
                buildId: e2f42d2b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                assistantId: befbca99-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                status: started
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - ownerFeedback must be shorter than or equal to 2000 characters
                  - ownerFeedback must be a string
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such assistant, or it was never built and so has no prompt to refine.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Assistant has no build artifacts to refine
                error: Not Found
                statusCode: 404

  /realtime-assistants/builds/{assistantId}:
    get:
      operationId: getAssistantBuild
      tags:
        - Authoring an assistant
      summary: Get an assistant's build record
      description: >-
        The durable record behind an assistant's builds: the brief, prompt
        version, gate result, and chosen sample transcripts — it never expires.
        It is not live progress: `status` is whatever the last run wrote when it
        ended, so an in-flight refine still reads the previous outcome. Poll the
        build status for anything currently running.
      parameters:
        - name: assistantId
          in: path
          required: true
          description: >-
            The `assistantId` the build returned.
          schema:
            type: string
            format: uuid
          example: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The build record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantBuildRecord'
              examples:
                building:
                  summary: The pipeline is still running
                  value:
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    assistantId: b424880f-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    status: building
                    promptVersion: 1
                    brief:
                      organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      goal: payment_reminder
                      voiceId: v_meklc281
                      gender: female
                      context: >-
                        Bright Future School, Rawalpindi. Call parents about
                        the pending monthly fee, tell them the amount and the
                        due date, and ask when they can pay.
                      sampleRows:
                        - name: فرحان ملک
                          phone: '+923215558842'
                          amount: '12,500'
                          dueDate: 25 اگست
                    updatedAt: '2026-08-18T05:16:58.583Z'
                ready:
                  summary: >-
                    Finished after three rounds, with the grader still
                    objecting
                  value:
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    assistantId: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    status: ready
                    promptVersion: 3
                    passed: false
                    agentName: ثناء
                    businessName: Shifa Clinic
                    brief:
                      organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      goal: appointment
                      voiceId: v_meklc281
                      gender: female
                      context: >-
                        Shifa Clinic, Lahore. Call patients to confirm
                        tomorrow's doctor appointment, remind them of the time,
                        and offer to reschedule if they cannot come.
                      sampleRows:
                        - name: عائشہ صدیقی
                          phone: '+923001234567'
                          doctor: ڈاکٹر عمران شیخ
                          appointmentTime: کل صبح 11 بجے
                    sampleTranscripts:
                      happy:
                        label: >-
                          عائشہ صدیقی — cheerful appointment confirmation for
                          Dr. عمران شیخ, Lahore
                        archetype: happy
                        path: happy
                        turns:
                          - speaker: agent
                            text: |-
                              السلام علیکم، کیا میں عائشہ صدیقی سے بات کر رہی ہوں؟

                              میں Shifa Clinic سے ثناء بات کر رہی ہوں۔ کل کی appointment confirm کرنی تھی، ایک منٹ بات کر سکتی ہوں؟
                          - speaker: caller
                            text: >-
                              وعلیکم السلام بھئی جی، جی میں عائشہ صدیقی ہی بول
                              رہی ہوں۔ ہاں جی بالکل، آپ ایک منٹ بات کر لیں۔
                          - speaker: agent
                            text: |-
                              شکریہ عائشہ۔

                              آپ کی کل صبح گیارہ بجے ڈاکٹر عمران شیخ کے ساتھ appointment ہے۔

                              آپ اُس وقت آ سکیں گی؟
                          - speaker: caller
                            text: >-
                              جی ہاں بھئی! میں کل صبح 11 بجے بالکل آ جاؤں گی،
                              اپائنٹمنٹ کنفرم کر دیں۔ خدا حافظ
                    updatedAt: '2026-08-18T05:13:44.008Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No build record under that id in the API key's project — an
            assistant created by hand rather than by a build has none.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: >-
                  No build found for assistant
                  c723a299-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                error: Not Found
                statusCode: 404

  /realtime-assistants/{realtimeAssistantId}/session:
    post:
      operationId: createAssistantSession
      tags:
        - Starting a conversation
      summary: Create a web session token
      description: >-
        Mint a LiveKit token for a browser or mobile client to talk to a stored
        assistant. It is a standard LiveKit token — connect to `wsUrl` with any
        LiveKit client SDK or UI component library and the assistant joins the
        room on its own. The token is good for the assistant's
        `config.session.ttl`, 600 seconds by default — that caps how long the
        client has to connect, not how long the conversation can run, so mint
        one token per conversation. The session record is written when the token
        is minted, so an unused token still leaves a session behind.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSessionRequest'
            example:
              participantName: عائشہ صدیقی
              roomName: shifa-clinic
      responses:
        '200':
          description: The join token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionToken'
              example:
                token: eyJhbGciOiJIUzI1NiJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6…
                wsUrl: wss://upliftai-prod-yd34to8b.livekit.cloud
                roomName: shifa-clinic-f2c14cfc-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - participantName should not be empty
                  - participantName must be a string
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: >-
            The organization is out of credits. No session is created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Insufficient credits to start a realtime session
                error: Payment Required
                statusCode: 402
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/VersionNotReady'

  /realtime-assistants/{realtimeAssistantId}/public-session:
    post:
      operationId: createPublicSession
      tags:
        - Starting a conversation
      security: []
      summary: Open a session without an API key
      description: >-
        Open a web session with no API key, so client-side code can mint its own
        token. Works only for assistants marked `public`, which makes the
        assistant id the entire credential: anyone who reads it out of your page
        source can start sessions billed to your organization. The assistant
        joins the room once the client connects.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: fa13c857-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePublicSessionRequest'
            example:
              participantName: عائشہ صدیقی
              roomName: shifa-clinic-demo
      responses:
        '200':
          description: >-
            Credentials for the browser client. The agent joins once the
            client connects.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionToken'
              example:
                token: eyJhbGciOiJIUzI1NiJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6InNoaWZhLWNsaW5pYy1kZS02Mjcw…
                wsUrl: wss://upliftai-prod-yd34to8b.livekit.cloud
                roomName: shifa-clinic-de-6270e96c-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - participantName should not be empty
                  - participantName must be a string
                error: Bad Request
                statusCode: 400
        '402':
          description: >-
            The assistant's organization is out of credits. No session is created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Insufficient credits to start a realtime session
                error: Payment Required
                statusCode: 402
        '403':
          description: >-
            The assistant is not `public`. Once it has been published, this
            reads the published version, so turning `public` on takes an update
            and then a publish.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Assistant is not available publicly
                error: Forbidden
                statusCode: 403
        '404':
          description: No assistant with that id in any project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Realtime assistant not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
        '503':
          $ref: '#/components/responses/VersionNotReady'

  /realtime-assistants/adhoc/session:
    post:
      operationId: createAdhocSession
      tags:
        - Starting a conversation
      summary: Start a session from inline config
      description: |-
        Identical to [Create a web session token](/api-reference/starting-a-conversation/create-a-web-session-token) — you get a LiveKit `wsUrl` and token, and everything on that page applies. The difference: the assistant is created on the fly from the `config` you send, instead of using a stored id. It stays hidden from the stored-assistant routes and **is gone five hours later**.

        The inline `config` follows the same rules as [Create an assistant](/api-reference/assistants/create-an-assistant): provider and model strings resolve when the client connects, so an unsupported value kills that conversation rather than failing this request. For Urdu, tell the model to reply in Nastaliq script, not Roman Urdu.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAdhocSessionRequest'
            example:
              participantName: ayesha-web
              roomName: shifa-clinic
              config:
                agent:
                  instructions: "آپ شفا کلینک کی ہیلپ ڈیسک ایجنٹ ہیں۔ ویب وزیٹر کے سوالات کا جواب دیں: اوقاتِ کار، ڈاکٹروں کی دستیابی، اور اپائنٹمنٹ بکنگ۔ صرف نستعلیق اردو میں جواب دیں۔"
                  initialGreeting: true
                  greetingInstructions: "السلام علیکم کہیں، شفا کلینک کا تعارف کروائیں، پھر پوچھیں کہ کس طرح مدد کر سکتے ہیں۔"
                session:
                  ttl: 900
                stt:
                  default:
                    provider: soniox
                    model: stt-rt-preview
                    language: ur
                tts:
                  default:
                    provider: upliftai
                    voiceId: v_meklc281
                    outputFormat: MP3_22050_32
                llm:
                  default:
                    provider: google
                    model: gemini-2.5-flash
      responses:
        '200':
          description: Credentials for the browser client to join with.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionToken'
              example:
                token: eyJhbGciOiJIUzI1NiJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6…
                wsUrl: wss://upliftai-prod-yd34to8b.livekit.cloud
                roomName: shifa-clinic-835050e4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: >-
            The organization is out of credits. No session is created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Insufficient credits to start a realtime session
                error: Payment Required
                statusCode: 402
        '403':
          $ref: '#/components/responses/Forbidden'

  /realtime-assistants/{realtimeAssistantId}/call:
    post:
      operationId: createAssistantCall
      tags:
        - Starting a conversation
      summary: Dispatch a call from this assistant
      description: |-
        Dial a customer as this assistant. `dispatched` means the carrier accepted the call and it is about to ring, not that anyone picked up. The returned `callId` is also the `sessionId`: [poll the session status](/api-reference/sessions-%26-call-records/get-a-sessions-status) for ringing, answer, and outcome. The response's `externalRef` is the telephony reference to quote in support tickets.

        Send an `Idempotency-Key` on retries so a timed-out request can't place a second call; the key also makes the `callId` start with `api_` instead of being a fresh UUID.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Caller-chosen key that fixes the call id, so a retried request
            cannot dial twice. Scoped to this assistant.
          schema:
            type: string
            maxLength: 256
          example: shifa-reminder-2026-08-18-ayesha-siddiqi
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssistantCallRequest'
            example:
              toNumber: '+923001234567'
              variables:
                patientName: عائشہ صدیقی
                doctorName: ڈاکٹر عمران شیخ
                appointmentTime: کل صبح 11 بجے
              additionalInstructions: "مریضہ کا نام عائشہ صدیقی ہے، اپائنٹمنٹ کل صبح 11 بجے ڈاکٹر عمران شیخ کے ساتھ ہے۔ اگر وہ کل نہ آ سکیں تو جمعرات دوپہر 3 بجے کا متبادل وقت پیش کریں۔"
      responses:
        '200':
          description: The call is dialing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantCallResponse'
              example:
                callId: session_N2MzZjlhMDIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                externalRef: AD_xxxxxxxxxxxx
                status: dispatched
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                badNumber:
                  summary: toNumber is neither E.164 nor a local leading-zero number
                  value:
                    message:
                      - 'toNumber must be E.164 (e.g., +14155551234) or local format (e.g., 03001234567)'
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: The organization is out of credits; nothing was dialed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  statusCode:
                    type: integer
              example:
                message: Insufficient credits to start a realtime session
                statusCode: 402
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >-
            Nothing was dialed. On `duplicate_in_flight` poll the returned `callId` instead of retrying; on `number_busy` retry once the live call ends.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CallConflict'
              example:
                error: duplicate_in_flight
                callId: session_N2MzZjlhMDIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
        '429':
          description: >-
            The organization is at its concurrent outbound call limit, every outbound caller ID is at line capacity, or your project exceeded its request rate limit (150 per minute by default). Nothing was dialed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  statusCode:
                    type: integer
              example:
                message: Organization concurrent outbound call limit reached
                statusCode: 429
        '503':
          $ref: '#/components/responses/VersionNotReady'

  /realtime-assistants/{realtimeAssistantId}/sessions:
    get:
      operationId: listAssistantSessions
      tags:
        - Sessions & call records
      summary: List an assistant's sessions
      description: >-
        Every web session and phone call one assistant handled, newest first.
        Filters are applied after each page is read, so a page can come back
        short or even empty with more behind it — keep paging until `nextCursor`
        is absent. Records are kept for 90 days.
      parameters:
        - name: realtimeAssistantId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 452dda41-ea9d-458f-a14c-a4d0f1c8a342
        - name: channel
          in: query
          schema:
            type: string
            enum: [web, telephony, whatsapp]
          example: telephony
        - name: direction
          in: query
          description: Web sessions are always `inbound` — the user opens them.
          schema:
            type: string
            enum: [inbound, outbound]
        - name: state
          in: query
          schema:
            type: string
            enum:
              - created
              - dispatched
              - dialing
              - ringing
              - answered
              - active
              - completed
              - failed
          example: completed
        - name: from
          in: query
          description: >-
            Inclusive lower bound on `createdAt`. Compared as a raw string against the ISO-8601 UTC timestamps in the response, so a non-ISO value silently shifts or voids the range instead of erroring.
          schema:
            type: string
            format: date-time
          example: '2026-08-16T00:00:00.000Z'
        - name: to
          in: query
          description: Inclusive upper bound on `createdAt`, same format as `from`.
          schema:
            type: string
            format: date-time
          example: '2026-08-17T00:00:00.000Z'
        - name: limit
          in: query
          description: >-
            Sessions read per page before filtering. Values above the maximum are clamped, not rejected.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: >-
            The previous page's `nextCursor`, passed back unchanged. Treat it as opaque.
          schema:
            type: string
        - name: assistantAlias
          in: query
          description: >-
            `draft` lists only sessions launched on the draft. `prod` lists
            everything else, including sessions launched by an exact version
            number and sessions from before versions existed.
          schema:
            type: string
            enum: [prod, draft]
        - name: include_summary
          in: query
          required: false
          description: >-
            Adds each call's `summary` to its row, up to about 1 KB each. Must
            be exactly `true` or `false`, anything else is a 400. A row with no
            summary leaves the key out: a call that never connected, one not
            summarised yet, or one from before summaries.
          schema:
            type: boolean
            default: false
          example: true
      responses:
        '200':
          description: One page of sessions, newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListSessionsResponse'
              example:
                sessions:
                  - sessionId: session_MjU2NjEzNTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                    realtimeAssistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    channel: telephony
                    direction: outbound
                    state: completed
                    connected: true
                    toNumber: '+923001234567'
                    fromNumber: '+924238900100'
                    roomName: call-25661352-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    participantIdentity: callee-25661352
                    transportProvider: twilio
                    variables:
                      source: assistant-test
                    createdAt: '2026-08-16T18:55:22.677Z'
                    ringingAt: '2026-08-16T18:55:30.758Z'
                    connectedAt: '2026-08-16T18:55:36.927Z'
                    answeredAt: '2026-08-16T18:55:37.641Z'
                    endedAt: '2026-08-16T18:56:17.411Z'
                    durationSec: 40
                  - sessionId: session_MTM4ZDY1MDgteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                    realtimeAssistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    channel: web
                    direction: inbound
                    state: created
                    connected: false
                    outcome: no_answer
                    roomName: web-138d6508-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    participantIdentity: عائشہ صدیقی
                    createdAt: '2026-08-18T05:14:25.444Z'
                nextCursor: eyJyZWFsdGltZUFzc2lzdGFudElkIjogIjQ1MmRkYTQxLXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCIsICJzZXNzaW9uSWQiOiAiMTM4ZDY1MDgteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwgImNyZWF0ZWRBdCI6ICIyMDI2LTA4LTE4VDA1OjE0OjI1LjQ0NFoifQ
        '400':
          description: A filter value is outside its enum.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - 'state must be one of the following values: created, dispatched, dialing, ringing, answered, active, completed, failed'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /realtime-assistants/sessions/{sessionId}:
    get:
      operationId: getSession
      tags:
        - Sessions & call records
      summary: Get a session's status
      description: >-
        Session metadata — where one browser session or phone call stands
        right now. Poll this while a call is in flight; once it ends, read the
        [session
        detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes)
        for the transcript, tool calls, and grade.
      parameters:
        - name: sessionId
          in: path
          required: true
          description: >-
            The `callId` from the call dispatch, a `sessionId` from the sessions
            list, or a `callId` from a campaign's calls list or a webhook.
          schema:
            type: string
          example: session_MjU2NjEzNTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
        - name: include_audio_url
          in: query
          required: false
          description: >-
            Adds `audioUrl`, a recording link valid for one hour. Any value switches it on, including `false`, so omit the parameter instead. The link is signed without checking a recording exists, so it 404s on calls that were never recorded.
          schema:
            type: boolean
          example: true
      responses:
        '200':
          description: The session as it stands right now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRecord'
              examples:
                completedCall:
                  summary: An outbound call that finished — 40 seconds of talk time
                  value:
                    sessionId: session_MjU2NjEzNTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                    realtimeAssistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    channel: telephony
                    direction: outbound
                    state: completed
                    connected: true
                    toNumber: '+923001234567'
                    fromNumber: '+924232591000'
                    roomName: call-25661352-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    participantIdentity: callee-25661352
                    transportProvider: twilio
                    variables:
                      patientName: عائشہ صدیقی
                      appointmentTime: کل صبح 11 بجے
                    createdAt: '2026-08-16T18:55:22.677Z'
                    ringingAt: '2026-08-16T18:55:30.758Z'
                    connectedAt: '2026-08-16T18:55:36.927Z'
                    answeredAt: '2026-08-16T18:55:37.641Z'
                    endedAt: '2026-08-16T18:56:17.411Z'
                    durationSec: 40
                neverConnected:
                  summary: A web session that never connected, so `outcome` is a placeholder
                  value:
                    sessionId: session_OTJmZTUzZGMteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                    realtimeAssistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    channel: web
                    direction: inbound
                    state: created
                    connected: false
                    outcome: no_answer
                    roomName: web-92fe53dc-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    participantIdentity: ayesha-siddiqi
                    createdAt: '2026-08-18T05:14:24.198Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No session with that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Session not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /realtime-assistants/sessions/{sessionId}/detail:
    get:
      operationId: getSessionDetail
      tags:
        - Sessions & call records
      summary: Get a call's transcript and outcomes
      description: >-
        Read this after a call ends. The transcript comes from the post-call
        report, so it is empty on calls that never connected and briefly empty
        on one that just hung up; `grade`, `conversion` and `callback` come from
        that same pipeline, so treat all three as optional. While a call is
        still live, poll [`/realtime-assistants/sessions/{sessionId}`](/api-reference/sessions-%26-call-records/get-a-sessions-status) instead.
      parameters:
        - name: sessionId
          in: path
          required: true
          description: >-
            The `callId` from the call dispatch, a `sessionId` from the sessions
            list, or a `callId` from a campaign's calls list or a webhook.
          schema:
            type: string
          example: session_MjU2NjEzNTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
        - name: include_audio_url
          in: query
          required: false
          description: >-
            Adds `audioUrl` to the response. Must be exactly `true` or
            `false` — `1`, `TRUE` and a bare `?include_audio_url` are all
            400s.
          schema:
            type: boolean
            default: false
          example: true
      responses:
        '200':
          description: The call.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionDetail'
              examples:
                completedCall:
                  summary: A finished outbound call
                  value:
                    sessionId: session_MjU2NjEzNTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                    realtimeAssistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    channel: telephony
                    direction: outbound
                    state: completed
                    connected: true
                    toNumber: '+923001234567'
                    fromNumber: '+924232591000'
                    roomName: call-25661352-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    participantIdentity: callee-25661352
                    transportProvider: twilio
                    variables:
                      source: assistant-test
                    createdAt: '2026-08-16T18:55:22.677Z'
                    ringingAt: '2026-08-16T18:55:30.758Z'
                    connectedAt: '2026-08-16T18:55:36.927Z'
                    answeredAt: '2026-08-16T18:55:37.641Z'
                    endedAt: '2026-08-16T18:56:17.411Z'
                    durationSec: 40
                    transcript:
                      - speaker: caller
                        text: 'ہیلو،'
                        atSec: 1
                      - speaker: agent
                        text: 'ہیلو، میں ڈاکٹر عامرہ ہوں۔ میں خوابوں کی تعبیر اور ان کے پوشیدہ معنی سمجھنے میں آپ کی مدد کروں گی۔ آج کون سا خواب آپ کو لے کر آیا ہے؟'
                        atSec: 6
                      - speaker: caller
                        text: 'ٹھیک ہے۔'
                        atSec: 7
                      - speaker: caller
                        text: I was having a good dream. Do you know what a good dream is?
                        atSec: 18
                      - speaker: agent
                        text: 'جی ہاں، بالکل۔ اچھا خواب یا خوشگوار خواب وہ ہوتا ہے جس میں انسان کو سکون، خوشی، راحت یا امید محسوس ہو۔'
                        atSec: 25
                    toolCalls: []
                    summary:
                      headline: Caller asked what a good dream means, got an explanation, and did not book a session.
                      bullets:
                        - Caller opened by saying they had a good dream
                        - Agent explained that a good dream leaves peace, happiness or hope
                        - Call ended after the explanation with nothing arranged
                      facts:
                        - label: Next Step
                          value: None
                        - label: Topic
                          value: Meaning of a good dream
                      language: en
                      promptVersion: call-summary-v1
                      model: gpt-5.4-2026-03-05
                      generatedAt: '2026-08-16T18:56:19.104Z'
                neverConnected:
                  summary: A web session that never connected
                  value:
                    sessionId: session_Yzk5NjQyMDQteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4
                    realtimeAssistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    channel: web
                    direction: inbound
                    state: created
                    connected: false
                    outcome: no_answer
                    roomName: shifa-clinic-de-c9964204-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    participantIdentity: عائشہ صدیقی
                    createdAt: '2026-08-18T05:14:08.525Z'
                    transcript: []
                    toolCalls: []
        '400':
          description: '`include_audio_url` was neither `true` nor `false`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Validation failed (boolean string is expected)
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No session with that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Session not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /realtime-assistants/sessions/{sessionId}/listen:
    post:
      operationId: listenToSession
      tags:
        - Sessions & call records
      summary: Listen in on a live call
      description: >-
        Get a LiveKit token for the room a live call is already running in. The
        listener joins hidden and subscribe-only — they can hear the call but
        not speak into it — and the token expires ten minutes after it is
        issued. Live transcription arrives on the `lk.transcription` text stream
        from the moment you connect, with nothing backfilled.
      parameters:
        - name: sessionId
          in: path
          required: true
          description: The session to listen to. Ids come from the dispatch that started the call or the assistant's session list.
          schema:
            type: string
            format: uuid
          example: 425700ce-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: A listener token for the call's existing room.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionToken'
              example:
                token: eyJhbGciOiJIUzI1NiJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6IndlYi00MjU3MDBjZS14eHh4…xxxx
                wsUrl: wss://upliftai-prod-yd34to8b.livekit.cloud
                roomName: web-425700ce-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        '400':
          description: >-
            The session has no room to join — it has already ended, or has
            not reached one yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Session is not live (state: completed)'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such session in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Session not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /campaigns:
    post:
      operationId: createCampaign
      tags:
        - Campaigns
      summary: Create a campaign
      description: >-
        The campaign lands in `draft` and dials nothing until you launch it.
        You don't need the full config up front: a draft can be created
        without an `assistant.assistantId` and [updated
        field by field](/api-reference/campaigns/update-a-campaign) until
        launch, which is where missing pieces are rejected. What you do send is
        checked now: a `contacts.listId` that doesn't exist fails this request.
        All the ways contacts get into a campaign:
        [Contacts](/voice-agents/campaigns/contacts).
        The one choice you can't revisit is `mode` — `bounded` or `continuous`
        is fixed at creation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCampaignRequest'
            example:
              name: Edversity cybersecurity intake — August
              mode: continuous
              config:
                goal: lead
                assistant:
                  assistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                dialing:
                  lines: 1
                  fromNumber: '+924238900100'
                  retryPolicy:
                    maxAttempts: 3
                    retryOn: [no_answer, busy, voicemail]
                    backoffMinutes: [15, 120]
                schedule:
                  callingWindow:
                    from: '10:00'
                    to: '19:00'
                    tz: Asia/Karachi
      responses:
        '201':
          description: The campaign as stored, in `draft`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: 5170facc-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Edversity cybersecurity intake — August
                state: draft
                mode: continuous
                config:
                  goal: lead
                  assistant:
                    assistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  dialing:
                    lines: 1
                    fromNumber: '+924238900100'
                    retryPolicy:
                      maxAttempts: 3
                      retryOn: [no_answer, busy, voicemail]
                      backoffMinutes: [15, 120]
                  schedule:
                    callingWindow:
                      from: '10:00'
                      to: '19:00'
                      tz: Asia/Karachi
                trigger:
                  kind: list
                steps:
                  - call
                createdAt: '2026-08-18T05:20:28.892Z'
                updatedAt: '2026-08-18T05:20:28.892Z'
                version: 1
        '400':
          description: >-
            Validation failed — a malformed `name` or `mode`, an appointment
            goal with no calendar connection, or an invalid scorecard.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Unknown campaign mode: hybrid'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            The contact list or calendar connection named in `config` does not exist in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Contact list not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

    get:
      operationId: listCampaigns
      tags:
        - Campaigns
      summary: List campaigns
      description: Every campaign in the API key's project, a page at a time.
      parameters:
        - name: limit
          in: query
          required: false
          description: >-
            Campaigns per page. Above 100 is clamped to 100, and a value that
            isn't a positive number falls back to 50 instead of erroring.
          schema:
            type: integer
            default: 50
          example: 25
        - name: cursor
          in: query
          required: false
          description: >-
            The previous response's `nextCursor`. Opaque — pass it back
            unmodified.
          schema:
            type: string
      responses:
        '200':
          description: One page of campaigns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  campaigns:
                    type: array
                    items:
                      $ref: '#/components/schemas/Campaign'
                  nextCursor:
                    type: string
                    description: >-
                      Absent once the walk is done. A full page still carries a cursor and the last page can be empty, so keep paging until `nextCursor` disappears rather than until a page looks short. The list is read live, so a campaign created mid-walk can shift between pages.
              example:
                campaigns:
                  - campaignId: cb5401ef-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Edversity admissions — September intake
                    state: running
                    mode: continuous
                    config:
                      goal: lead
                      assistant:
                        assistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      contacts:
                        listId: 3b7f21ac-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      schedule:
                        callingWindow:
                          from: '10:00'
                          to: '19:00'
                          tz: Asia/Karachi
                      dialing:
                        lines: 2
                        fromNumber: '+924238900100'
                        retryPolicy:
                          maxAttempts: 3
                          retryOn: [no_answer, busy, voicemail]
                          backoffMinutes: [15, 120]
                    trigger:
                      kind: list
                    steps:
                      - call
                    currentRunId: d1403129-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    runStartedAt: '2026-08-18T05:16:02.711Z'
                    createdAt: '2026-08-18T05:15:56.059Z'
                    updatedAt: '2026-08-18T05:16:02.725Z'
                    version: 3
                  - campaignId: 521e8ca1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Shifa Clinic appointment reminders — 19 Aug
                    state: launch_failed
                    mode: bounded
                    config:
                      goal: appointment
                    trigger:
                      kind: list
                    steps:
                      - call
                    currentRunId: d6bf8d72-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    runStartedAt: '2026-08-18T05:16:20.122Z'
                    lastError:
                      phase: validate
                      message: no contact list configured
                      at: '2026-08-18T05:16:22.287Z'
                    createdAt: '2026-08-18T05:16:19.119Z'
                    updatedAt: '2026-08-18T05:16:22.287Z'
                    version: 5
                  - campaignId: 43150de2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Al-Falah credit card payment reminders
                    state: draft
                    mode: bounded
                    config: {}
                    trigger:
                      kind: list
                    steps:
                      - call
                    createdAt: '2026-08-18T05:15:42.611Z'
                    updatedAt: '2026-08-18T05:15:42.611Z'
                    version: 1
                nextCursor: eyJwayI6ImExMmI3ZTc0LXh4eHgjYzc4MmE0ZWMteHh4eCIsInNrIjoiNDMxNTBkZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4In0
        '400':
          description: The `cursor` could not be decoded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /campaigns/{campaignId}:
    get:
      operationId: getCampaign
      tags:
        - Campaigns
      summary: Get a campaign
      description: >-
        Read a campaign's `state` here before calling pause, resume, launch or
        close — each of those only works from specific states (pause needs
        `running`, resume needs `paused`) and returns a 409 from any other.
        Polling a running campaign also catches a scheduled launch firing, or a
        pause the platform applied on its own.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: a49f0c00-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The campaign.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              examples:
                draft:
                  summary: Created, nothing dialed yet
                  value:
                    campaignId: a49f0c00-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Shifa Clinic appointment reminders — August
                    state: draft
                    config:
                      assistant:
                        assistantId: 83a4d18c-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    trigger:
                      kind: list
                    steps:
                      - call
                    mode: bounded
                    createdAt: '2026-08-18T04:48:42.748Z'
                    updatedAt: '2026-08-18T04:48:42.748Z'
                    version: 1
                running:
                  summary: Launched and dialing
                  value:
                    campaignId: 7c1f9ab2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Al-Falah credit card payment reminders
                    state: running
                    config:
                      goal: payment_reminder
                      assistant:
                        assistantId: 80b00435-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      contacts:
                        listId: 4e2c8d61-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                      schedule:
                        callingWindow:
                          from: '10:00'
                          to: '19:00'
                          tz: Asia/Karachi
                      dialing:
                        lines: 1
                        fromNumber: '+924232567890'
                        retryPolicy:
                          maxAttempts: 3
                    trigger:
                      kind: list
                    steps:
                      - call
                    mode: bounded
                    currentRunId: b83d5f07-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    runStartedAt: '2026-08-18T05:12:03.441Z'
                    createdAt: '2026-08-17T22:40:11.208Z'
                    updatedAt: '2026-08-18T05:12:04.907Z'
                    version: 6
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No campaign with that id in the API key's project — a campaign in another project reads the same. Malformed ids land here too; the path is never validated as a UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

    patch:
      operationId: updateCampaign
      tags:
        - Campaigns
      summary: Update a campaign
      description: |-
        What you can edit depends on the state:

        - `draft`, `launch_failed` — everything
        - `scheduled` — everything except `goal` and `assistant`
        - `launching`, `running`, `paused`, `draining` — only `assistantVersion`, `schedule.callingWindow`, `dialing`, `tools` and `delivery`; `name` is frozen too
        - `completed` — nothing

        Config sections replace rather than merge, so send each section back whole. `schedule` is the exception: `scheduledFor` and `callingWindow` are kept independently.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 43150de2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCampaignRequest'
            example:
              config:
                dialing:
                  lines: 6
                  fromNumber: '+924238900100'
                schedule:
                  callingWindow:
                    from: '10:00'
                    to: '18:00'
                    tz: Asia/Karachi
      responses:
        '200':
          description: >-
            The whole campaign as stored after the update, not just the sections
            you sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: 43150de2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Al-Falah credit card payment reminder
                state: draft
                mode: bounded
                config:
                  goal: payment_reminder
                  assistant:
                    assistantId: 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  schedule:
                    scheduledFor: '2027-01-15T09:00:00.000Z'
                    callingWindow:
                      from: '10:00'
                      to: '18:00'
                      tz: Asia/Karachi
                  dialing:
                    lines: 6
                    fromNumber: '+924238900100'
                  scorecard:
                    rubricVersionId: alfalah-reminder-v1
                    rules:
                      - ruleId: identified_bank
                        title: "بینک کا تعارف"
                        instruction: "ایجنٹ نے کال کے آغاز میں الفلاح بینک کا نام لیا"
                        points: 30
                        scoringType: binary
                        evidenceRequired: true
                      - ruleId: stated_due_date
                        title: "آخری تاریخ بتائی"
                        instruction: "ایجنٹ نے واجب الادا رقم اور ادائیگی کی آخری تاریخ بتائی"
                        points: 40
                        scoringType: binary
                        evidenceRequired: true
                      - ruleId: logged_promise
                        title: "ادائیگی کا وعدہ"
                        instruction: "اگر کسٹمر ادائیگی کا وعدہ کرے تو ایجنٹ نے تاریخ نوٹ کی"
                        points: 30
                        scoringType: binary
                        evidenceRequired: false
                trigger:
                  kind: list
                steps:
                  - call
                createdAt: '2026-08-18T05:15:42.611Z'
                updatedAt: '2026-08-18T05:22:25.445Z'
                version: 12
        '400':
          description: >-
            Editing a section frozen in this state, a blank `name`, an invalid `scorecard`, an `assistantVersion` the assistant never published, or an `appointment` campaign whose calendar connection is missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                frozenSection:
                  summary: Section not editable in this state
                  value:
                    message: "Field 'goal' is not editable while campaign is 'running'"
                    error: Bad Request
                    statusCode: 400
                blankName:
                  summary: Name was whitespace only
                  value:
                    message: Campaign name cannot be blank
                    error: Bad Request
                    statusCode: 400
                rubricPoints:
                  summary: Scorecard rules do not total 100
                  value:
                    message: 'rubric: points sum to 70, must be 100'
                    error: Bad Request
                    statusCode: 400
                unpublishedVersion:
                  summary: Pinned to a version the assistant never published
                  value:
                    message: Assistant 452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx has no published version 7
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such campaign in the API key's project — or the `contacts.listId`
            you sent names no list in it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /campaigns/{campaignId}/contacts:
    post:
      operationId: appendCampaignContacts
      tags:
        - Campaigns
      summary: Append contacts to a campaign
      description: >-
        The only way to add contacts after launch — the other ways in:
        [Contacts](/voice-agents/campaigns/contacts). Only a `continuous`
        campaign that is `running` or `paused` takes them. Draft, completed,
        closed and bounded campaigns all 409 — a bounded campaign's contacts are
        fixed at the snapshot taken at launch. A bad row is rejected on its own
        — the rest of the batch still lands, and the response is a 200 either
        way. Read `summary` for what was actually accepted and why the rest
        were excluded. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 270411aa-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppendContactsRequest'
            examples:
              inline:
                summary: Two new orders and one row with a broken number
                value:
                  contacts:
                    - externalRef: ord-48812
                      name: عائشہ صدیقی
                      phone: '+923001234567'
                      attributes:
                        order: '48812'
                        amount: Rs 4,500
                        city: لاہور
                    - externalRef: ord-47190
                      name: بلال احمد
                      phone: '03219876543'
                      attributes:
                        order: '47190'
                        amount: Rs 12,000
                    - externalRef: ord-48814
                      name: فرحان قریشی
                      phone: 0300-123
              fromSheet:
                summary: Re-pull a connected sheet, phone column named by hand
                value:
                  sheet:
                    ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Orders
                    connectionId: 7f3c1a90-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  mapping:
                    phoneColumn: Mobile No.
      responses:
        '200':
          description: >-
            A 200 says the batch was processed, not that every row was accepted — read `summary`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppendContactsResponse'
              example:
                results:
                  - contactId: evt#ord-48812
                    status: enrolled
                  - contactId: evt#ord-47190
                    status: rearmed
                  - status: skipped
                    reason: invalidPhone
                summary:
                  enrolled: 1
                  rearmed: 1
                  alreadyPending: 0
                  inFlight: 0
                  skipped: 1
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                noSource:
                  summary: No source given (an empty `contacts` reads the same)
                  value:
                    message: contacts must not be empty
                    error: Bad Request
                    statusCode: 400
                twoSources:
                  summary: More than one source given
                  value:
                    message: Provide exactly one of contacts, uploadId, or sheet
                    error: Bad Request
                    statusCode: 400
                tooLarge:
                  summary: Over the per-request cap
                  value:
                    message: 'batch_too_large: at most 1000 contacts per request'
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such campaign, or no such upload or connection for the source you gave.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                campaign:
                  value:
                    message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                    error: Not Found
                    statusCode: 404
                upload:
                  value:
                    message: 'Upload not found: 00000000-0000-4000-8000-000000000000'
                    error: Not Found
                    statusCode: 404
        '409':
          description: >-
            The campaign cannot take contacts. `message` says which
            precondition failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notContinuous:
                  summary: A bounded campaign — its contacts are fixed at launch
                  value:
                    message: campaign_not_continuous
                    error: Conflict
                    statusCode: 409
                notActive:
                  summary: Not yet launched, or already completed
                  value:
                    message: campaign_not_active
                    error: Conflict
                    statusCode: 409
                closed:
                  summary: Closed — it is draining and takes no new work
                  value:
                    message: campaign_closed
                    error: Conflict
                    statusCode: 409

  /campaigns/{campaignId}/schedule:
    post:
      operationId: scheduleCampaign
      tags:
        - Campaigns
      summary: Schedule a campaign launch
      description: >-
        The campaign moves to `scheduled` and sits there until `scheduledFor`,
        when the platform launches it — even outside the calling window;
        dialing then waits for the window to open. The contact list is
        snapshotted at that launch moment, not now: edits to the list before
        `scheduledFor` make it in, edits after never reach the campaign. Only a
        `draft` campaign can be scheduled — to move a time already set, PATCH
        `config.schedule.scheduledFor`.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: bf9d18e9-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleCampaignRequest'
            example:
              scheduledFor: '2026-12-24T09:30:00.000Z'
      responses:
        '200':
          description: The campaign, now `scheduled`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: bf9d18e9-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic December appointment reminders
                state: scheduled
                config:
                  assistant:
                    assistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  contacts:
                    listId: 7c1f4a90-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  schedule:
                    scheduledFor: '2026-12-24T09:30:00.000Z'
                trigger:
                  kind: list
                steps:
                  - call
                mode: bounded
                createdAt: '2026-08-18T05:18:26.271Z'
                updatedAt: '2026-08-18T05:18:54.747Z'
                version: 2
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - scheduledFor must be a valid ISO 8601 date string
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No campaign with that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
        '409':
          description: >-
            The campaign is not in `draft`. The message names the state it is
            in.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'illegal campaign transition: scheduled --schedule-->'
                error: Conflict
                statusCode: 409

  /campaigns/{campaignId}/launch:
    post:
      operationId: launchCampaign
      tags:
        - Campaigns
      summary: Launch a campaign now
      description: |-
        Dialing starts as soon as the calling window allows.

        **The contact list is snapshotted at launch.** The configured list is copied into the run, so rows you add or remove from it afterward never reach this campaign. On a `continuous` campaign, [append contacts to the campaign](/api-reference/campaigns/append-contacts-to-a-campaign) instead.

        Launch works from three states: `draft`, `scheduled` (fires it early), and `launch_failed` (retries it). Launching a scheduled campaign early leaves `scheduledFor` sitting in the config, but it will not fire a second time.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: cb5401ef-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The campaign is `running` and its run has been created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignLaunchResult'
              examples:
                bounded:
                  summary: A bounded campaign enrolling its list
                  value:
                    runId: b83d5f07-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    enrolled: 1240
                continuous:
                  summary: >-
                    A continuous campaign launched before any contacts were added
                  value:
                    runId: d1403129-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    enrolled: 0
        '400':
          description: >-
            The campaign cannot be launched as configured — no `assistant.assistantId`, no `config.contacts.listId` on a bounded campaign, or `dialing.lines` set below 1. The campaign is left in `launch_failed`, not `draft` — fix the config and launch again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Campaign has no contact list to launch
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such campaign in the API key's project — or the configured
            contact list no longer exists (`Contact list not found: …`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
        '409':
          description: >-
            The campaign is already `running`, `paused`, `draining` or `completed`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'illegal campaign transition: running --launch-->'
                error: Conflict
                statusCode: 409

  /campaigns/{campaignId}/pause:
    post:
      operationId: pauseCampaign
      tags:
        - Campaigns
      summary: Pause campaign dialing
      description: >-
        The dialer stops claiming new contacts, but calls already in flight run
        to the end — expect live calls for a few more minutes. Only a `running`
        campaign can pause; anything else, a second pause included, is a 409.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 6bea26ad-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The campaign, now `paused`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: 6bea26ad-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic appointment reminders
                state: paused
                config:
                  assistant:
                    assistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  dialing:
                    lines: 1
                    fromNumber: '+924238900001'
                trigger:
                  kind: list
                steps:
                  - call
                currentRunId: 21948771-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                runStartedAt: '2026-08-18T05:17:13.241Z'
                mode: continuous
                createdAt: '2026-08-18T05:17:02.013Z'
                updatedAt: '2026-08-18T05:17:17.720Z'
                version: 4
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The campaign is not `running`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'illegal campaign transition: draft --pause-->'
                error: Conflict
                statusCode: 409

  /campaigns/{campaignId}/resume:
    post:
      operationId: resumeCampaign
      tags:
        - Campaigns
      summary: Resume a paused campaign
      description: >-
        The dialer starts claiming contacts again within a few seconds, and
        `statusMessage` is cleared. Only a `paused` campaign can resume —
        anything else, `running` included, is a 409. A campaign the platform
        paused for credits will pause again on the next credit failure.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 7655e63e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The campaign, now `running`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: 7655e63e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: "الفلاح کارڈ ادائیگی یاد دہانی — اگست"
                state: running
                mode: bounded
                config:
                  assistant:
                    assistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  contacts:
                    listId: 9f14b2ae-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  dialing:
                    lines: 3
                    fromNumber: '+922135791468'
                trigger:
                  kind: list
                steps:
                  - call
                currentRunId: 7428b0cd-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                runStartedAt: '2026-08-18T05:17:03.584Z'
                createdAt: '2026-08-18T05:16:55.654Z'
                updatedAt: '2026-08-18T05:17:12.717Z'
                version: 5
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such campaign in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
        '409':
          description: The campaign is not `paused`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'illegal campaign transition: running --resume-->'
                error: Conflict
                statusCode: 409

  /campaigns/{campaignId}/close:
    post:
      operationId: closeCampaign
      tags:
        - Campaigns
      summary: Close a continuous campaign
      description: >-
        How a continuous campaign ends: it accepts no more contacts and
        completes once queued work and in-flight calls finish. Idempotent,
        allowed from any state a continuous campaign can be in, leaves `state`
        untouched, and cannot be undone. Closing before launch still lets you
        launch — it just dials the contacts it already has. Bounded campaigns
        409, since they complete on their own once the list snapshotted at
        launch runs out.
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: d0ad2a3d-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The campaign, with `closedAt` stamped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                campaignId: d0ad2a3d-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic appointment reminders
                state: running
                config:
                  assistant:
                    assistantId: 26932cc0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                  dialing:
                    lines: 1
                    fromNumber: '+924238900001'
                trigger:
                  kind: list
                steps:
                  - call
                currentRunId: a1b8534a-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                runStartedAt: '2026-08-18T05:18:28.029Z'
                mode: continuous
                closedAt: '2026-08-18T05:18:32.991Z'
                createdAt: '2026-08-18T05:18:14.845Z'
                updatedAt: '2026-08-18T05:18:32.991Z'
                version: 4
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Only continuous campaigns can be closed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: >-
                  Campaign a49f0c00-xxxx-xxxx-xxxx-xxxxxxxxxxxx is not
                  continuous — bounded campaigns complete on their own
                error: Conflict
                statusCode: 409

  /campaigns/{campaignId}/stats:
    get:
      operationId: getCampaignStats
      tags:
        - Campaign outcomes
      summary: Aggregate stats for a campaign
      description: |-
        Counters for every run of the campaign, written by the post-call pipeline — all zero until the first call finishes.

        **Note:** Scores, conversions, and sentiment are computed by LLMs — best effort, with room to be wrong. We are continuously closing the tracking gaps, but for sensitive workloads we recommend running your own analysis on the transcripts delivered over [webhooks](/voice-agents/webhooks/overview).
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: cb5401ef-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The campaign's counters — zero across the board before the first call completes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignStats'
              example:
                dialed: 412
                connected: 168
                booked: 74
                notInterested: 51
                wrongPerson: 9
                voicemail: 96
                noAnswer: 121
                silentPickup: 19
                failed: 6
                analysisFailed: 2
                callbacks: 12
                scored: 168
                avgScore: 76
                great: 83
                ok: 61
                low: 24
                totalDurationSec: 9284
                avgDurationSec: 55
                totalContacts: 850
                attemptedContacts: 318
                resolvedContacts: 262
                retrying: 44
                exhausted: 33
                projectedRevenue: 333000
                sentiment:
                  happy: 91
                  neutral: 52
                  annoyed: 14
                  unknown: 11
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No campaign under that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /campaigns/{campaignId}/results:
    get:
      operationId: getCampaignResults
      tags:
        - Campaign outcomes
      summary: List a campaign's outcomes
      description: |-
        One row per connected call, carrying the outcome post-call analysis recorded — a booked appointment, a qualified lead, an RSVP, whatever the campaign's goal defines as a result. The shape of each outcome and the labels to render it under follow that goal; `vocabulary` tells your UI which set applies. Contacts never dialed or never answered are not here — they live in [`/campaigns/{campaignId}/calls`](/api-reference/campaign-outcomes/list-a-campaigns-calls) — and a campaign with no scorecard produces no rows at all.

        **Note:** Scores, conversions, and sentiment are computed by LLMs — best effort, with room to be wrong. We are continuously closing the tracking gaps, but for sensitive workloads we recommend running your own analysis on the transcripts delivered over [webhooks](/voice-agents/webhooks/overview).
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: a5be4d9e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: limit
          in: query
          description: Rows per page. Values above 100 are clamped, not rejected.
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: >-
            The `nextCursor` of the previous page.
          required: false
          schema:
            type: string
      responses:
        '200':
          description: One page of outcomes, newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignResults'
              example:
                artifactModel: lead_list
                vocabulary:
                  conversionVerb: qualified
                  conversionNoun: lead
                rows:
                  - sessionId: session_YTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDEyMzQ1NjcjYTE
                    goalKind: lead
                    name: عائشہ خان
                    phone: '+923001234567'
                    runId: d1403129-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    payload:
                      kind: lead
                      status: qualified
                      interest: سائبر سیکیورٹی ڈپلومہ
                      nextStep: ڈیمو سیشن کے لیے رابطہ
                      summary: پروگرام میں دلچسپی ہے، فیس اور شیڈول بتا دیا گیا۔
                    createdAt: '2026-08-18T05:16:47.324Z'
                  - sessionId: session_YTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IzAzMjE0NTY3ODkwI2Ey
                    goalKind: lead
                    name: بلال احمد
                    phone: '03214567890'
                    runId: d1403129-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    payload:
                      kind: lead
                      status: callback
                      notes: شام چھ بجے دوبارہ کال کرنے کو کہا۔
                    createdAt: '2026-08-18T05:11:02.881Z'
        '400':
          description: The `cursor` could not be decoded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /campaigns/{campaignId}/calls:
    get:
      operationId: listCampaignCalls
      tags:
        - Campaign outcomes
      summary: List a campaign's calls
      description: |-
        One row per enrolled contact per campaign run, showing that contact's latest attempt — never-dialed contacts included. Earlier attempts collapse into the latest one, and a relaunch or recurrence can put the same `contactId` on the page twice. Rows come in no particular order, so don't read the top of the list as the most recent. Where `callId` is present it is the session id. Pass it to the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes) for that call's transcript and recording.

        **Note:** Scores, conversions, and sentiment are computed by LLMs — best effort, with room to be wrong. We are continuously closing the tracking gaps, but for sensitive workloads we recommend running your own analysis on the transcripts delivered over [webhooks](/voice-agents/webhooks/overview).
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: cb5401ef-98f7-49e7-8a6e-822fc54e697f
        - name: limit
          in: query
          required: false
          description: >-
            Rows per page. No maximum, though response size caps every page whether you set this or not.
          schema:
            type: integer
            minimum: 1
          example: 50
        - name: cursor
          in: query
          required: false
          description: The previous page's `nextCursor`, verbatim.
          schema:
            type: string
        - name: include_summary
          in: query
          required: false
          description: >-
            Adds each call's `summary` to its row, up to about 1 KB each. Must
            be exactly `true` or `false`, anything else is a 400. A row with no
            summary leaves the key out: a call that never connected, one not
            summarised yet, or one from before summaries.
          schema:
            type: boolean
            default: false
          example: true
      responses:
        '200':
          description: One page of the campaign's contacts and their latest call.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignCallList'
              examples:
                populated:
                  summary: A graded call, a busy number waiting on its retry, and a contact not yet dialed
                  value:
                    calls:
                      - callId: session_N2MxZjlhYjQteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDEyMzQ1NjcjYTE
                        contactId: '+923001234567'
                        name: عائشہ صدیقی
                        phone: '+923001234567'
                        state: done
                        attempts: 1
                        nextAttemptAt: 1787030462711
                        connected: true
                        startedAt: '2026-08-18T05:16:02.711Z'
                        durationSec: 74
                        score: 82
                        sentiment: happy
                      - callId: session_N2MxZjlhYjQteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMyMTQ0NTU2NjEjYTI
                        contactId: '+923214455661'
                        name: بلال احمد
                        phone: '+923214455661'
                        state: retry_wait
                        attempts: 2
                        nextAttemptAt: 1787034062711
                        outcome: busy
                        connected: false
                        startedAt: '2026-08-18T05:31:44.902Z'
                        endReason: busy:486
                        failureReason: busy
                        failureReasonWithCode: busy:486
                      - contactId: '+923339988774'
                        name: فرحان قریشی
                        phone: '+923339988774'
                        state: queued
                        attempts: 0
                        nextAttemptAt: 1787030400000
                    nextCursor: eyJjYW1wYWlnbklkIjoiN2MxZjlhYjQtxxxx
                empty:
                  summary: A campaign with nothing enrolled yet
                  value:
                    calls: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such campaign in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /campaigns/{campaignId}/callbacks:
    get:
      operationId: listCampaignCallbacks
      tags:
        - Campaign outcomes
      summary: List a campaign's callback requests
      description: |-
        The contacts who asked to be called again, soonest first. Post-call analysis opens these — nothing in the API creates one. Every status comes back, and there is no status query parameter — so to build a "who still needs a call" queue, filter to `open` in your own code.

        `kind: human` means a person must call. `kind: later` means the assistant may call again: on a campaign it requeues the contact for `callbackAfter` (`status: scheduled`), and the callback becomes `done` when a redial reaches the contact or `failed` when the redial cycle runs out.

        **Note:** Scores, conversions, and sentiment are computed by LLMs — best effort, with room to be wrong. We are continuously closing the tracking gaps, but for sensitive workloads we recommend running your own analysis on the transcripts delivered over [webhooks](/voice-agents/webhooks/overview).
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: cb5401ef-98f7-49e7-8a6e-822fc54e697f
        - name: kind
          in: query
          required: false
          description: >-
            Only this kind. `human` also returns callbacks from before kinds existed, which were all for a person. A filtered page can be short, or empty, and still have `nextCursor`.
          schema:
            type: string
            enum: [human, later]
        - name: limit
          in: query
          required: false
          description: Rows per page. Larger values are clamped to 100.
          schema:
            type: integer
            minimum: 1
            default: 50
        - name: cursor
          in: query
          required: false
          description: The `nextCursor` returned by the previous page.
          schema:
            type: string
      responses:
        '200':
          description: One page of callback requests, soonest first — by `callbackAfter` where there is one, `createdAt` otherwise.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CallbackList'
              example:
                callbacks:
                  - callbackId: callback_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDQ1Njc4OTAjYTEjY2I
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    assistantId: 80b00435-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    campaignId: 7d41c9e2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    sessionId: session_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDQ1Njc4OTAjYTE
                    kind: human
                    reason: Cardholder disputes the late fee and wants an agent to go through the statement.
                    requestedText: mujhe kisi banday se baat karni hai, kal subah
                    callbackAfter: '2026-10-29T11:00:00+05:00'
                    timezone: Asia/Karachi
                    status: open
                    timeline:
                      - type: created
                        at: '2026-10-28T06:41:19.204Z'
                      - type: note
                        at: '2026-10-28T07:02:10.000Z'
                        by: user_2Nq8
                        text: Statement pulled, calling after 11.
                    createdAt: '2026-10-28T06:41:19.204Z'
                    updatedAt: '2026-10-28T07:02:10.000Z'
                  - callbackId: callback_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMyMTg3NjU0MzIjYTIjY2I
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    assistantId: 80b00435-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    campaignId: 7d41c9e2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    sessionId: session_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMyMTg3NjU0MzIjYTI
                    kind: later
                    reason: Driving, asked to be called tomorrow at 3 pm.
                    requestedText: abhi drive kar raha hoon, kal 3 baje call karein
                    callbackAfter: '2026-10-29T15:00:00+05:00'
                    timezone: Asia/Karachi
                    status: scheduled
                    redial:
                      status: scheduled
                      at: '2026-10-29T15:00:00+05:00'
                    timeline:
                      - type: created
                        at: '2026-10-28T09:12:00.000Z'
                      - type: redial
                        at: '2026-10-28T09:12:00.000Z'
                        redial:
                          status: scheduled
                          at: '2026-10-29T15:00:00+05:00'
                    createdAt: '2026-10-28T09:12:00.000Z'
                    updatedAt: '2026-10-28T09:12:00.000Z'
        '400':
          description: The `cursor` was not one this API issued, or `kind` is not `human` or `later`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such campaign in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /campaigns/callbacks/{callbackId}:
    patch:
      operationId: updateCallbackStatus
      tags:
        - Campaign outcomes
      summary: Update a callback's status
      description: >-
        Set a callback `done` as your team works through them, or `open` to
        reopen it. Works on any status, `scheduled` and `failed` included.
        Setting the status it already has changes nothing. Each change is
        added to the callback's `timeline` with who made it. Setting a
        `scheduled` callback `done` does not cancel the assistant's redial.
      parameters:
        - name: callbackId
          in: path
          required: true
          description: >-
            The `callbackId` from the callbacks list or a callback webhook.
          schema:
            type: string
          example: callback_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDQ1Njc4OTAjYTEjY2I
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCallbackStatusRequest'
            example:
              status: done
      responses:
        '200':
          description: The callback's status after the write.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateCallbackStatusResponse'
              example:
                callbackId: callback_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDQ1Njc4OTAjYTEjY2I
                status: done
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - 'status must be one of the following values: open, done'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No callback with that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Callback not found: 7d41c9e2-xxxx-xxxx-xxxx-xxxxxxxxxxxx#+923004567890#a1#cb'
                error: Not Found
                statusCode: 404
        '409':
          description: >-
            Reopening a callback whose `timeline` already holds 100 events. Setting `done` is never refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Callback timeline is full: 7d41c9e2-xxxx-xxxx-xxxx-xxxxxxxxxxxx#+923004567890#a1#cb'
                error: Conflict
                statusCode: 409

  /campaigns/callbacks/{callbackId}/notes:
    post:
      operationId: addCallbackNote
      tags:
        - Campaign outcomes
      summary: Add a note to a callback
      description: >-
        Adds a note to the callback's `timeline`, for example what your team
        tried. Notes are kept in order and cannot be edited or deleted. Returns
        the whole callback.
      parameters:
        - name: callbackId
          in: path
          required: true
          description: >-
            The `callbackId` from the callbacks list or a callback webhook.
          schema:
            type: string
          example: callback_N2Q0MWM5ZTIteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDQ1Njc4OTAjYTEjY2I
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddCallbackNoteRequest'
            example:
              text: Statement pulled, calling after 11.
      responses:
        '200':
          description: The callback, with the note at the end of its `timeline`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Callback'
        '400':
          description: >-
            `text` is missing, blank, or over 2,000 bytes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - text must not be blank
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No callback with that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Callback not found: 7d41c9e2-xxxx-xxxx-xxxx-xxxxxxxxxxxx#+923004567890#a1#cb'
                error: Not Found
                statusCode: 404
        '409':
          description: The callback's `timeline` already holds 100 events.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Callback timeline is full: 7d41c9e2-xxxx-xxxx-xxxx-xxxxxxxxxxxx#+923004567890#a1#cb'
                error: Conflict
                statusCode: 409

  /campaigns/{campaignId}/conversions:
    get:
      operationId: listCampaignConversions
      tags:
        - Campaign outcomes
      summary: List a campaign's conversion records
      description: |-
        One row per call that post-call analysis scored as an outcome — never-dialed, unanswered, and ungraded contacts are absent. Newest first. This route returns the stored record as-is; [`/campaigns/{campaignId}/results`](/api-reference/campaign-outcomes/list-a-campaigns-outcomes) serves the same rows joined to each contact's name and phone and labelled with the campaign's goal vocabulary, which is the easier one to render.

        **Note:** Scores, conversions, and sentiment are computed by LLMs — best effort, with room to be wrong. We are continuously closing the tracking gaps, but for sensitive workloads we recommend running your own analysis on the transcripts delivered over [webhooks](/voice-agents/webhooks/overview).
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: a5be4d9e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: limit
          in: query
          required: false
          description: Rows per page. Values above 100 are capped to 100.
          schema:
            type: integer
            default: 50
        - name: cursor
          in: query
          required: false
          description: The `nextCursor` from the previous page.
          schema:
            type: string
      responses:
        '200':
          description: One page of conversion records.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListConversionsResponse'
              examples:
                records:
                  summary: A lead campaign's first page
                  value:
                    conversions:
                      - sessionId: session_YTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDEyMzQ1NjcjYTE
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        assistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        campaignId: a5be4d9e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        ref: session_YTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDEyMzQ1NjcjYTE
                        goalKind: lead
                        payload:
                          kind: lead
                          status: qualified
                          interest: سائبر سیکیورٹی ڈپلومہ، شام والی کلاس
                          nextStep: جمعرات شام 6 بجے ڈیمو سیشن
                          notes: فیس کی قسطوں کے بارے میں پوچھا
                          summary: عائشہ صدیقی کورس میں دلچسپی رکھتی ہیں اور ڈیمو کے لیے تیار ہیں۔
                        date: '2026-08-18'
                        createdAt: '2026-08-18T09:12:44.831Z'
                      - sessionId: session_YTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMyMTQ0NTU2NjEjYTI
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        assistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        campaignId: a5be4d9e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        ref: session_YTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMyMTQ0NTU2NjEjYTI
                        goalKind: lead
                        payload:
                          kind: lead
                          status: callback
                          interest: بی ایس سی کے بعد کے آپشنز
                          nextStep: کل دوپہر دوبارہ کال کریں
                          summary: بلال احمد ابھی مصروف تھے، کل بات کرنے کو کہا۔
                        date: '2026-08-18'
                        createdAt: '2026-08-18T08:47:02.118Z'
                    nextCursor: eyJwayI6ICJhMTJiN2U3NC14eHh4LXh4eHgteHh4eC14eHh4eHh4eHh4eHgjYzc4MmE0ZWMteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwgInNrIjogImE1YmU0ZDllLXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCMrOTIzMjE0NDU1NjYxI2EyIiwgImNhbXBhaWduSWQiOiAiYTViZTRkOWUteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwgInNsb3RPckNyZWF0ZWQiOiAiMjAyNi0wOC0xOFQwODo0NzowMi4xMThaIn0
                empty:
                  summary: Nothing graded yet
                  value:
                    conversions: []
        '400':
          description: The `cursor` could not be decoded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No campaign under that id in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Campaign not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /contact-lists/uploads:
    post:
      operationId: uploadContactFile
      tags:
        - Contacts
      summary: Upload a contact file
      description: >-
        Step one of the file flow. The API detects CSV vs `.xlsx` from the
        contents, reads only the first sheet of a workbook, parses the rows and
        stores them — no contact list yet. Pass the returned `uploadId` to
        [`/contact-lists`](/api-reference/contacts/create-a-contact-list) to create one, or re-preview first if the detected
        mapping needs correcting. A missing name or phone column is not an
        upload error: it lands in `mappingIssues` and those rows count as
        excluded. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: >-
                    The contact file, up to 10 MB. CSV or `.xlsx`, detected from the contents rather than the filename. Only the first sheet of a workbook is read.
              required: [file]
      responses:
        '201':
          description: >-
            The stored upload and what parsing it produced. A missing name or phone column is not an error — it lands in `mappingIssues` and the affected rows count as excluded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactUploadPreview'
              example:
                uploadId: dac00330-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                summary:
                  willBeCalled: 2
                  excluded:
                    invalidPhone: 0
                    missingName: 0
                    duplicate: 0
                    dnc: 0
                    recentlyCalled: 0
                  errors: []
                detectedMapping:
                  nameColumn: Name
                  phoneColumn: Phone
                  attributeColumns:
                    - City
                sample:
                  - name: عائشہ صدیقی
                    phone: '+923000000001'
                    attributes:
                      City: لاہور
                  - name: بلال احمد
                    phone: '+923000000002'
                    attributes:
                      City: کراچی
                total: 2
                columns:
                  - Name
                  - Phone
                  - City
                mappingIssues: []
        '400':
          description: >-
            No `file` part was sent — an empty one counts as none — or it is neither CSV nor a readable `.xlsx`. Legacy `.xls` must be re-saved as `.xlsx` or CSV.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'multipart `file` is required'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          description: The file is larger than 10 MB. Nothing is stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: File too large
                error: Payload Too Large
                statusCode: 413

  /contact-lists/uploads/{uploadId}/preview:
    post:
      operationId: previewContactUpload
      tags:
        - Contacts
      summary: Re-preview an upload with a mapping
      description: >-
        Try a column mapping before you commit to it. Nothing is written, so
        re-run this as often as you need while adjusting — a body is still
        required, so send `{}` to just re-run auto-detection. When the sample
        looks right, create the list from the same `uploadId` and mapping. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).
      parameters:
        - name: uploadId
          in: path
          required: true
          description: The `uploadId` returned by [`/contact-lists/uploads`](/api-reference/contacts/upload-a-contact-file).
          schema:
            type: string
            format: uuid
          example: 3343ebcc-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewUploadRequest'
            example:
              mapping:
                nameColumn: Name
                phoneColumn: B
                attributeColumns:
                  - City
      responses:
        '201':
          description: >-
            The parse under the requested mapping.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactPreview'
              example:
                summary:
                  willBeCalled: 2
                  excluded:
                    invalidPhone: 0
                    missingName: 0
                    duplicate: 0
                    dnc: 0
                    recentlyCalled: 0
                  errors: []
                detectedMapping:
                  nameColumn: Name
                  phoneColumn: Phone
                  attributeColumns:
                    - City
                sample:
                  - name: عائشہ صدیقی
                    phone: '+923000000001'
                    attributes:
                      City: لاہور
                  - name: بلال احمد
                    phone: '+923000000002'
                    attributes:
                      City: کراچی
                total: 2
                columns:
                  - Name
                  - Phone
                  - City
                  - Appointment
                mappingIssues: []
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - mapping must be an object
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such upload in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Upload not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /contact-lists:
    post:
      operationId: createContactList
      tags:
        - Contacts
      summary: Create a contact list
      description: >-
        Turn rows into a stored list you can call: an upload from
        [`/contact-lists/uploads`](/api-reference/contacts/upload-a-contact-file), inline CSV or pasted lines, or a Google Sheet.
        The import validates rows and deduplicates by phone number, keeping only
        contacts that can be dialed. Pass the returned `listId` as a campaign's
        `config.contacts.listId`. A list is never edited — to call a different
        set of people, import again. To see the outcome before creating:
        [dry-run](/api-reference/contacts/dry-run-a-contact-import)
        inline rows or a sheet, [re-preview](/api-reference/contacts/re-preview-an-upload-with-a-mapping)
        an uploaded file. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactListRequest'
            examples:
              uploadedFile:
                summary: From an uploaded file
                value:
                  name: Al-Falah payment reminders — August
                  kind: csv_upload
                  uploadId: f352c8d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
              pastedRows:
                summary: From pasted rows
                value:
                  name: Walk-in leads — Tuesday
                  kind: paste
                  data: "عائشہ صدیقی, +923001234567\nبلال احمد, 03214567890"
              googleSheet:
                summary: One-time pull of a Google Sheet
                value:
                  name: Event invitees — Annual Dinner
                  kind: google_sheets
                  ref: '1kQ7bP2xLd9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx!Contacts'
      responses:
        '201':
          description: The stored list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactList'
              example:
                listId: 590e4881-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Al-Falah payment reminders — August
                sourceKind: csv_upload
                sourceRef: f352c8d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                summary:
                  willBeCalled: 2
                  excluded:
                    invalidPhone: 1
                    missingName: 1
                    duplicate: 1
                    dnc: 0
                    recentlyCalled: 0
                  errors:
                    - row: 4
                      issue: duplicate
                      value: '+923001234567'
                    - row: 5
                      issue: missingName
                      value: '+923451234567'
                    - row: 6
                      issue: invalidPhone
                      value: '0300123'
                contactCount: 2
                createdAt: '2026-08-18T05:20:09.138Z'
                updatedAt: '2026-08-18T05:20:09.138Z'
                version: 1
        '400':
          description: Validation failed, or the request carried no rows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                validation:
                  summary: The body failed validation
                  value:
                    message:
                      - name must be shorter than or equal to 120 characters
                      - name should not be empty
                      - name must be a string
                    error: Bad Request
                    statusCode: 400
                noRows:
                  summary: Neither uploadId nor data was sent
                  value:
                    message: 'csv_upload requires `data`'
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such upload in the API key's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Upload not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

    get:
      operationId: listContactLists
      tags:
        - Contacts
      summary: List contact lists
      description: >-
        One page of your contact lists, newest first. Each list's `summary` and
        `contactCount` are a snapshot of the import that created it — never
        recomputed, so they tell you nothing about how a campaign using the list
        is going.
      parameters:
        - name: limit
          in: query
          required: false
          description: >-
            Lists per page. Anything above 100 is clamped to 100; a zero,
            negative or non-numeric value falls back to the default.
          schema:
            type: integer
            default: 50
          example: 25
        - name: cursor
          in: query
          required: false
          description: >-
            The previous page's `nextCursor`. Opaque — pass it back verbatim.
          schema:
            type: string
      responses:
        '200':
          description: One page of contact lists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactListPage'
              example:
                lists:
                  - listId: a82ee571-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Edversity warm leads
                    sourceKind: paste
                    summary:
                      willBeCalled: 2
                      excluded:
                        invalidPhone: 0
                        missingName: 0
                        duplicate: 0
                        dnc: 0
                        recentlyCalled: 0
                      errors: []
                    contactCount: 2
                    createdAt: '2026-08-18T05:18:05.363Z'
                    updatedAt: '2026-08-18T05:18:05.363Z'
                    version: 1
                  - listId: 894ab1c2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: "شفا کلینک — کل کے مریض"
                    sourceKind: csv_upload
                    summary:
                      willBeCalled: 3
                      excluded:
                        invalidPhone: 1
                        missingName: 0
                        duplicate: 1
                        dnc: 0
                        recentlyCalled: 0
                      errors:
                        - row: 5
                          issue: duplicate
                          value: '+923001234567'
                        - row: 6
                          issue: invalidPhone
                          value: '0300'
                    contactCount: 3
                    createdAt: '2026-08-18T05:17:55.237Z'
                    updatedAt: '2026-08-18T05:17:55.237Z'
                    version: 1
                nextCursor: eyJwayI6ImExMmI3ZTc0…
        '400':
          description: The `cursor` was not issued by this API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /contact-lists/{listId}:
    get:
      operationId: getContactList
      tags:
        - Contacts
      summary: Get a contact list's metadata
      description: >-
        A list's metadata — how many contacts were kept and which rows were
        dropped, as recorded when the list was created. It is never recomputed
        after import, so it is not campaign progress. For the contacts
        themselves, page [`/contact-lists/{listId}/contacts`](/api-reference/contacts/page-through-a-lists-contacts).
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 894ab1c2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The contact list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactList'
              example:
                listId: 894ab1c2-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic patients — 19 August
                sourceKind: csv_upload
                summary:
                  willBeCalled: 3
                  excluded:
                    invalidPhone: 1
                    missingName: 0
                    duplicate: 1
                    dnc: 0
                    recentlyCalled: 0
                  errors:
                    - row: 5
                      issue: duplicate
                      value: '+923001234567'
                    - row: 6
                      issue: invalidPhone
                      value: '0300'
                contactCount: 3
                createdAt: '2026-08-18T05:17:55.237Z'
                updatedAt: '2026-08-18T05:17:55.237Z'
                version: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No list with that id in the API key's project. A list in another project is invisible here, and a malformed id 404s rather than 400.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Contact list not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /contact-lists/{listId}/contacts:
    get:
      operationId: listContactListContacts
      tags:
        - Contacts
      summary: Page through a list's contacts
      description: >-
        The rows as they were saved when the list was created, not a fresh read
        of the source file or sheet. `q` filters after each page is read, so a
        page can come back empty while later pages still hold matches — keep
        going until `nextCursor` is absent. Pages are ordered by `contactId` as
        text rather than import order, and there is no sort parameter.
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 134ccad1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: q
          in: query
          required: false
          description: >-
            Case-insensitive substring match on `name` and `phone`. Values in `attributes` are not searched.
          schema:
            type: string
          example: ayesha
        - name: limit
          in: query
          required: false
          description: >-
            Rows read per page. Values above 100 are clamped to 100; anything non-numeric or below 1 falls back to 50 instead of erroring.
          schema:
            type: integer
            default: 50
          example: 2
        - name: cursor
          in: query
          required: false
          description: The previous page's `nextCursor`. Opaque — do not construct one.
          schema:
            type: string
      responses:
        '200':
          description: >-
            One page, ordered by `contactId` as text rather than import order, so `+92…` rows come before `03…` ones. There is no sort parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactPage'
              examples:
                firstPage:
                  summary: First page of a five-contact list, `?limit=2`
                  value:
                    contacts:
                      - listId: 134ccad1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        contactId: '+923001234567'
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        name: Ayesha Siddiqui
                        phone: '+923001234567'
                        attributes:
                          Doctor: Dr Imran Shaikh
                          AppointmentTime: کل صبح 11 بجے
                      - listId: 134ccad1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        contactId: '+923009876543'
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        name: Hamza Raza
                        phone: '+923009876543'
                        attributes:
                          Doctor: Dr Sana Tariq
                          AppointmentTime: کل شام 5 بجے
                    nextCursor: eyJjb250YWN0SWQiOiIrOTIzMDA5ODc2NTQzIiwibGlzdElkIjoiMTM0Y2NhZDEteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4In0
                filteredPageWithNoMatches:
                  summary: >-
                    `?q=zainab&limit=1` — the row read on this page did not
                    match, but Zainab is on a later one
                  value:
                    contacts: []
                    nextCursor: eyJjb250YWN0SWQiOiIrOTIzMDAxMjM0NTY3IiwibGlzdElkIjoiMTM0Y2NhZDEteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4In0
        '400':
          description: The `cursor` was not one this API issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such list in the API key's project. A list in another project returns the same 404, not a 403.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Contact list not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /contact-sources:
    post:
      operationId: createContactSource
      tags:
        - Google Sheet sources
      summary: Register a Google Sheet as a source
      description: |-
        Register a Google Sheet as a reusable contact source. The source is a pointer to the live sheet, not a copy: a campaign reads whatever rows the sheet holds at that moment, so to change the contacts you edit the sheet. The registration itself never changes — to point at a different sheet or use a different column mapping, register a new source.

        Nothing is checked at registration today (this may change): a wrong spreadsheet id, an unshared sheet, or an unknown `connectionId` all register fine and only fail later — so [preview the source](/api-reference/google-sheet-sources/re-check-a-registered-sheet) right after creating it.

        Only a Google Sheet can be registered here. For a CSV or pasted rows, [create a contact list](/api-reference/contacts/create-a-contact-list) instead — a one-time import rather than a live source. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactSourceRequest'
            examples:
              publicSheet:
                summary: Public link-shared sheet
                value:
                  name: Edversity admissions leads
                  kind: google_sheets
                  ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Admissions
                  mapping:
                    nameColumn: طالب علم کا نام
                    phoneColumn: موبائل
                    attributeColumns:
                      - پروگرام
                      - شہر
              privateSheet:
                summary: Private sheet via Google connection
                value:
                  name: Clinic patient roster
                  kind: google_sheets
                  ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Patients
                  connectionId: 7d21c9b4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '201':
          description: The registered source.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactSource'
              example:
                sourceId: c616feca-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Edversity admissions leads
                kind: google_sheets
                ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Admissions
                mapping:
                  nameColumn: طالب علم کا نام
                  phoneColumn: موبائل
                  attributeColumns:
                    - پروگرام
                    - شہر
                createdAt: '2026-08-18T05:19:03.311Z'
                updatedAt: '2026-08-18T05:19:03.311Z'
                version: 1
        '400':
          description: >-
            Validation failed, or `kind` was not `google_sheets`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - name must be shorter than or equal to 120 characters
                  - name should not be empty
                  - name must be a string
                  - "Only 'google_sheets' connected sources can be registered; use the preview endpoint for csv/paste uploads"
                  - ref should not be empty
                  - ref must be a string
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    get:
      operationId: listContactSources
      tags:
        - Google Sheet sources
      summary: List registered contact sources
      description: >-
        Lists the Google Sheets sources registered in this API key's project,
        ordered by `sourceId` — arbitrary, but stable across pages. Nothing here
        reads Google, so a deleted or unshared sheet still lists normally; only
        a preview tells you whether it still resolves. CSV and pasted imports
        aren't sources — those land in [`/contact-lists`](/api-reference/contacts/create-a-contact-list).
      parameters:
        - name: limit
          in: query
          required: false
          description: >-
            Rows per page. Bad values are corrected rather than rejected — above 100 clamps to 100, anything else falls back to 50.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          example: 25
        - name: cursor
          in: query
          required: false
          description: >-
            The previous page's opaque `nextCursor`, passed back unchanged.
          schema:
            type: string
      responses:
        '200':
          description: >-
            One page of sources. Page until `nextCursor` is absent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sources:
                    type: array
                    items:
                      $ref: '#/components/schemas/ContactSource'
                  nextCursor:
                    type: string
                    description: Absent on the last page.
              example:
                sources:
                  - sourceId: 7ffda4d7-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Zameen buyer leads
                    kind: google_sheets
                    ref: 1kQ9pR3vXbN7hLm2TzYeWc4Ad8FgJs6UvHn0BqCxRtEo
                    connectionId: 3f9c2b18-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    createdAt: '2026-08-18T05:18:20.703Z'
                    updatedAt: '2026-08-18T05:18:20.703Z'
                    version: 1
                  - sourceId: 4367e59c-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Shifa Clinic patient sheet
                    kind: google_sheets
                    ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Patients
                    mapping:
                      nameColumn: مریض کا نام
                      phoneColumn: فون نمبر
                      attributeColumns:
                        - ڈاکٹر
                        - اپائنٹمنٹ کا وقت
                    createdAt: '2026-08-18T05:18:08.018Z'
                    updatedAt: '2026-08-18T05:18:08.018Z'
                    version: 1
                nextCursor: eyJzayI6IjQzNjdlNTljLXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCIsInBrIjoiYTEyYjdlNzQteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4I2M3ODJhNGVjLXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCJ9
        '400':
          description: The `cursor` was not one this API issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /contact-sources/{sourceId}:
    get:
      operationId: getContactSource
      tags:
        - Google Sheet sources
      summary: Get a contact source's metadata
      description: >-
        The source's metadata — sheet ref, connection, column mapping — not
        the sheet's contacts. Campaign launches re-read the live sheet, so
        an old registration still dials today's rows. To see the rows the source
        produces right now, preview it.
      parameters:
        - name: sourceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 8420468d-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The source's registration record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactSource'
              example:
                sourceId: 8420468d-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Zameen leads (Lahore)
                kind: google_sheets
                ref: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms!Leads
                mapping:
                  nameColumn: Full Name
                  phoneColumn: Mobile
                  attributeColumns:
                    - City
                    - Property Type
                createdAt: '2026-08-18T05:17:53.388Z'
                updatedAt: '2026-08-18T05:17:53.388Z'
                version: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such source in the API key's project. Ids are not format-checked, so a malformed one lands here too.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Contact source not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /contact-sources/preview:
    post:
      operationId: previewContactSource
      tags:
        - Contacts
      summary: Dry-run a contact import
      description: |-
        See exactly who a campaign would dial before you commit. It takes the same inputs as [`/contact-lists`](/api-reference/contacts/create-a-contact-list): a Google Sheet `ref`, or inline `data` holding CSV text or pasted "Name, +phone" lines. Nothing is stored or registered, so run it as often as you like. The one input it doesn't take is an uploaded file — [re-preview the upload](/api-reference/contacts/re-preview-an-upload-with-a-mapping) instead.

        For sheets the read is live, and the sheet must be reachable: shared as anyone-with-the-link, or read through a Google `connectionId`. When it isn't, the 500 doesn't forward Google's reason — a wrong spreadsheet id, a missing tab, and an unshared file all look identical.

        Already registered the source? [Re-check it](/api-reference/google-sheet-sources/re-check-a-registered-sheet) with the stored mapping instead. Column naming rules: [Special column names](/voice-agents/campaigns/contacts#special-column-names).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewContactSourceRequest'
            examples:
              sheet:
                summary: Preview a Google Sheet
                value:
                  kind: google_sheets
                  ref: '1kQ7bP2xLd9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx!Contacts!A:E'
              paste:
                summary: Preview pasted rows
                value:
                  kind: paste
                  data: "عائشہ صدیقی, +923001234567\nبلال احمد, 03214567890"
      responses:
        '200':
          description: >-
            The sheet as it stands right now. `willBeCalled` plus every count in
            `excluded` reconciles to `total`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactSourcePreview'
              examples:
                detected:
                  summary: Columns detected, two rows dropped
                  value:
                    summary:
                      willBeCalled: 3
                      excluded:
                        invalidPhone: 1
                        missingName: 0
                        duplicate: 1
                        dnc: 0
                        recentlyCalled: 0
                      errors:
                        - row: 4
                          issue: duplicate
                          value: '03001234567'
                        - row: 5
                          issue: invalidPhone
                          value: '0345'
                    detectedMapping:
                      nameColumn: Name
                      phoneColumn: Mobile No.
                      attributeColumns:
                        - City
                        - Policy Amount
                        - Due Date
                    sample:
                      - name: عائشہ صدیقی
                        phone: '+923001234567'
                        attributes:
                          City: لاہور
                          Policy Amount: 'Rs 18,000'
                          Due Date: 25 اگست
                      - name: بلال احمد
                        phone: '03211234567'
                        attributes:
                          City: کراچی
                          Policy Amount: 'Rs 42,500'
                          Due Date: 27 اگست
                      - name: حمزہ شیخ
                        phone: '+923339876543'
                        attributes:
                          City: ملتان
                          Policy Amount: 'Rs 30,000'
                          Due Date: 30 اگست
                    total: 5
                    columns:
                      - Name
                      - Mobile No.
                      - City
                      - Policy Amount
                      - Due Date
                    mappingIssues: []
                phoneColumnMissing:
                  summary: Phone column renamed — nothing dialable
                  value:
                    summary:
                      willBeCalled: 0
                      excluded:
                        invalidPhone: 2
                        missingName: 0
                        duplicate: 0
                        dnc: 0
                        recentlyCalled: 0
                      errors:
                        - row: 2
                          issue: invalidPhone
                          value: ''
                        - row: 3
                          issue: invalidPhone
                          value: ''
                    detectedMapping:
                      nameColumn: Full Name
                      attributeColumns:
                        - Mobile Number Updated
                        - Branch
                    sample: []
                    total: 2
                    columns:
                      - Full Name
                      - Mobile Number Updated
                      - Branch
                    mappingIssues:
                      - phoneColumnNotFound
        '400':
          description: >-
            The request can't be resolved to rows — `ref` missing on a
            `google_sheets` preview, `data` missing on a `csv_upload` or
            `paste`, `connectionId` sent with a non-sheet kind, or `kind`
            outside the enum.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'google_sheets requires `ref`'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No connection under that `connectionId` in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Connection not found: 6f3d9a52-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
                error: Not Found
                statusCode: 404
        '500':
          description: >-
            The sheet could not be read: wrong spreadsheet id, no such tab, or it isn't shared with the platform. Google's own error is not forwarded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  statusCode:
                    type: integer
                  message:
                    type: string
              example:
                statusCode: 500
                message: Internal server error

  /contact-sources/{sourceId}/preview:
    post:
      operationId: previewRegisteredContactSource
      tags:
        - Google Sheet sources
      summary: Re-check a registered sheet
      description: >-
        See what a registered source's sheet would produce for a campaign right
        now. It re-reads the sheet live with the stored `ref` and `mapping`, and
        persists nothing — worth a run before you launch against a source you
        registered days ago. To try a different mapping, use
        [`/contact-sources/preview`](/api-reference/contacts/dry-run-a-contact-import).
      parameters:
        - name: sourceId
          in: path
          required: true
          description: The `sourceId` returned when the source was registered.
          schema:
            type: string
            format: uuid
          example: 8420468d-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The sheet as it reads right now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactSourcePreview'
              example:
                summary:
                  willBeCalled: 2
                  excluded:
                    invalidPhone: 1
                    missingName: 0
                    duplicate: 1
                    dnc: 0
                    recentlyCalled: 0
                  errors:
                    - row: 4
                      issue: invalidPhone
                      value: '0300-999'
                    - row: 5
                      issue: duplicate
                      value: '03001234567'
                detectedMapping:
                  nameColumn: Full Name
                  phoneColumn: Mobile
                  attributeColumns:
                    - City
                    - Property Type
                sample:
                  - name: عائشہ صدیقی
                    phone: '03001234567'
                    attributes:
                      City: لاہور
                      Property Type: 5 مرلہ گھر
                  - name: بلال احمد
                    phone: '+923215557788'
                    attributes:
                      City: کراچی
                      Property Type: اپارٹمنٹ
                total: 4
                columns:
                  - Full Name
                  - Mobile
                  - City
                  - Property Type
                mappingIssues: []
        '400':
          description: >-
            The source's connection cannot be used to read the sheet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Connection 6b1f90c4-xxxx-xxxx-xxxx-xxxxxxxxxxxx is not a Google Sheets connection'
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            No such source in the API key's project — or the source's Google
            connection no longer exists (`Connection not found: …`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Contact source not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404
        '500':
          description: >-
            The sheet could not be read — deleted, unshared, a `ref` range that no longer exists, or a revoked Google connection. The body does not say which.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                statusCode: 500
                message: Internal server error

  /connections:
    post:
      security:
        - portalToken: []
      operationId: createConnection
      tags:
        - Integrations & delivery health
      summary: Create a connection
      description: |-
        Registers an external system for the project — a webhook that receives outcomes, an endpoint that answers tool calls, or an account a tool books against. Pick the body variant for your `kind` below.

        Webhook connections need no further wiring: every call in the project delivers its events to every webhook connection, so a second webhook means a second copy of everything.

        <Info>**Building a webhook receiver?** The event payloads, envelope, signing and retry behavior are all documented in [Webhooks](/voice-agents/webhooks/overview).</Info>

        Creating a connection never probes it — `health.state` stays `unknown` until you [run a health check](/voice-agents/api-reference/integration/probe-a-connections-credentials). Connection routes take a portal session token only; an API key gets 403 whatever permissions it carries.

        `google_calendar` and `sheets` connections are created through our own frontend for now — they need Google's OAuth consent to access the calendar or sheet. A public link-shared sheet needs no connection at all — [register it as a source](/api-reference/google-sheet-sources/register-a-google-sheet-as-a-source) directly.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConnectionRequest'
            examples:
              webhook:
                summary: Webhook for call events
                value:
                  name: Shifa Clinic call outcomes
                  kind: webhook
                  credentials:
                    signingSecret: whsec_7f3a9c21e84b4d0fa6c5b83e
                  config:
                    url: https://hooks.shifaclinic.pk/uplift/call-outcomes
              toolEndpoint:
                summary: Tool endpoint for mid-call lookups
                value:
                  name: Shifa Clinic appointment lookups
                  kind: tool_endpoint
                  credentials:
                    signingSecret: whsec_2c9d41b7a0e64f8c93d1e5a7
                  config:
                    url: https://hooks.shifaclinic.pk/uplift/tools
      responses:
        '201':
          description: >-
            The stored connection, minus its credentials. Creating one never probes it, so `health.state` starts at `unknown` until you call [`/connections/{connectionId}/health`](/voice-agents/api-reference/integration/probe-a-connections-credentials).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connection'
              example:
                connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic call outcomes
                kind: webhook
                config:
                  url: https://hooks.shifaclinic.pk/uplift/call-outcomes
                health:
                  state: unknown
                createdAt: '2026-08-14T11:03:18.442Z'
                updatedAt: '2026-08-14T11:03:18.442Z'
                version: 1
                credentialsConfigured: true
        '400':
          description: >-
            `name` is empty or over 120 characters, `kind` is not one of the listed values, or `credentials`/`config` is not an object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            An API key was used. Connections are reachable only from a portal
            session token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Forbidden resource
                error: Forbidden
                statusCode: 403

    get:
      security:
        - portalToken: []
      operationId: listConnections
      tags:
        - Integrations & delivery health
      summary: List connections
      description: >-
        Pages through every connection in the project. Rows come back ordered by
        `connectionId` descending, not by when they were created, so the newest
        connection is not necessarily first. Each row's `health` is whatever the
        last probe stored, not a live check. Portal session token only; an API
        key gets 403. Keep passing `nextCursor` as `cursor` until it comes back
        absent.
      parameters:
        - name: limit
          in: query
          required: false
          description: >-
            Rows per page. Values above 100 clamp to 100; non-numeric or non-positive values fall back to 50. Neither is an error.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          example: 20
        - name: cursor
          in: query
          required: false
          description: '`nextCursor` from the previous page. Anything else is a 400.'
          schema:
            type: string
          example: eyJwayI6ICJhMTJiN2U3NC14eHh4LXh4eHgteHh4eC14eHh4eHh4eHh4eHgjYzc4MmE0ZWMteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwgInNrIjogIjNiNThhMmYxLXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCJ9
      responses:
        '200':
          description: A page of connections.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionList'
              examples:
                registry:
                  summary: Three connections in one project
                  value:
                    connections:
                      - connectionId: b04f6ac8-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        name: Edversity leads sheet
                        kind: sheets
                        config:
                          accountEmail: admissions@edversity.pk
                        health:
                          state: unknown
                        credentialsConfigured: true
                        createdAt: '2026-08-11T06:24:57.310Z'
                        updatedAt: '2026-08-11T06:24:57.310Z'
                        version: 1
                      - connectionId: 7e1c4d92-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        name: Shifa Clinic bookings
                        kind: google_calendar
                        config:
                          calendarId: appointments@shifa.com.pk
                          accountEmail: ops@shifa.com.pk
                        health:
                          state: healthy
                          lastCheckedAt: '2026-08-17T11:03:19.774Z'
                          detail: ok (200)
                        credentialsConfigured: true
                        createdAt: '2026-07-29T14:52:06.118Z'
                        updatedAt: '2026-08-17T11:02:41.905Z'
                        version: 3
                      - connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        name: Zameen CRM webhook
                        kind: webhook
                        config:
                          url: https://crm.zameen.com/hooks/uplift
                        health:
                          state: failed
                          lastCheckedAt: '2026-08-16T09:14:02.641Z'
                          detail: unexpected status 404
                        credentialsConfigured: true
                        createdAt: '2026-07-02T08:17:44.520Z'
                        updatedAt: '2026-07-02T08:17:44.520Z'
                        version: 1
                empty:
                  summary: Nothing connected yet
                  value:
                    connections: []
        '400':
          description: The `cursor` could not be decoded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A project API key was used.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Forbidden resource
                error: Forbidden
                statusCode: 403

  /connections/{connectionId}:
    get:
      security:
        - portalToken: []
      operationId: getConnection
      tags:
        - Integrations & delivery health
      summary: Get a connection
      description: >-
        Reads one connection — where outcomes get delivered, or which external
        account a tool books against. Credentials are never returned. The
        `health` block is cached from the last
        [`/connections/{connectionId}/health`](/voice-agents/api-reference/integration/probe-a-connections-credentials) probe, not checked when you read.
        Portal session token only; an API key gets 403 whatever permissions it
        carries.
      parameters:
        - name: connectionId
          in: path
          required: true
          description: The connection to read.
          schema:
            type: string
            format: uuid
          example: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      responses:
        '200':
          description: The connection, with its credentials redacted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connection'
              examples:
                webhook:
                  summary: A webhook that answered its last ping
                  value:
                    connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Shifa Clinic call outcomes
                    kind: webhook
                    config:
                      url: https://hooks.shifaclinic.pk/uplift/call-outcomes
                    health:
                      state: healthy
                      lastCheckedAt: '2026-08-18T09:14:02.771Z'
                      detail: ping 200
                    createdAt: '2026-08-14T11:03:18.442Z'
                    updatedAt: '2026-08-16T07:52:09.310Z'
                    version: 3
                    credentialsConfigured: true
                revoked:
                  summary: A Google Calendar whose token was revoked
                  value:
                    connectionId: 7c1de904-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                    name: Google Calendar
                    kind: google_calendar
                    config:
                      accountEmail: appointments@shifaclinic.pk
                      calendarId: primary
                    health:
                      state: failed_permanent
                      lastCheckedAt: '2026-08-18T09:14:05.118Z'
                      detail: Google refresh token was revoked
                    createdAt: '2026-07-29T06:41:55.203Z'
                    updatedAt: '2026-08-02T13:20:44.671Z'
                    version: 2
                    credentialsConfigured: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            An API key was used.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Forbidden resource
                error: Forbidden
                statusCode: 403
        '404':
          description: >-
            No such connection in the token's project. An id from another project reads as 404, not 403.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Connection not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

    post:
      security:
        - portalToken: []
      operationId: updateConnection
      tags:
        - Integrations & delivery health
      summary: Update a connection
      description: |-
        Rename a connection, rotate its credentials, or change its config. The kind is fixed. Omitted `credentials` are left alone, but omitted `config` is cleared — always resend the whole config object. Webhook URLs must be HTTPS and cannot point at loopback, private, or link-local hosts. Rotating a secret or editing a URL does not re-run the probe, so `health` stays stale until you call [`/connections/{connectionId}/health`](/voice-agents/api-reference/integration/probe-a-connections-credentials). Portal session token only; API keys get 403.

        For `google_calendar` and `sheets`, fresh credentials come from re-running Google's OAuth consent in our own frontend — rotate them there, not here. A public link-shared sheet needs no connection at all — [register it as a source](/api-reference/google-sheet-sources/register-a-google-sheet-as-a-source) directly.

        <Info>**Building a webhook receiver?** The event payloads, envelope, signing and retry behavior are all documented in [Webhooks](/voice-agents/webhooks/overview).</Info>
      parameters:
        - name: connectionId
          in: path
          required: true
          description: The connection to edit, returned by [`/connections`](/voice-agents/api-reference/integration/list-connections).
          schema:
            type: string
            format: uuid
          example: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateConnectionRequest'
            example:
              name: Shifa Clinic call events
              credentials:
                signingSecret: whsec_7f2a9c14xxxxxxxxxxxxxxxxxxxxxxxx
              config:
                url: https://hooks.shifaclinic.pk/uplift/call-events
      responses:
        '200':
          description: The connection as stored. Credentials are never returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Connection'
              example:
                connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                name: Shifa Clinic call events
                kind: webhook
                config:
                  url: https://hooks.shifaclinic.pk/uplift/call-events
                credentialsConfigured: true
                health:
                  state: healthy
                  lastCheckedAt: '2026-08-17T11:02:39.514Z'
                  detail: ping 200
                createdAt: '2026-08-04T06:18:52.771Z'
                updatedAt: '2026-08-18T09:41:07.226Z'
                version: 3
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message:
                  - name must be shorter than or equal to 120 characters
                  - name should not be empty
                  - name must be a string
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Returned for any project API key, valid or not, and for a request with no bearer token. These routes take a portal session token only.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Forbidden resource
                error: Forbidden
                statusCode: 403
        '404':
          description: No such connection in the caller's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Connection not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /connections/{connectionId}/health:
    post:
      security:
        - portalToken: []
      operationId: checkConnectionHealth
      tags:
        - Integrations & delivery health
      summary: Probe a connection's credentials
      description: >-
        Runs a live check with the connection's stored credentials and saves the
        result on the connection. A failed probe still returns 200, so read
        `state`, not the HTTP status. What the probe does depends on the kind:
        `webhook` and `tool_endpoint` POST a signed ping to `config.url` with the same
        `x-uplift-ai-signature` header as a real delivery and accept any 2xx;
        `google_calendar` refreshes OAuth and reads one event from the
        configured calendar. `sheets` is never probed and stays `unknown`.
        `failed` is transient and worth retrying; `failed_permanent` means the
        credential was rejected and the connection has to be re-authorized.
        `detail` is free text, not a stable code.
      parameters:
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: 3b58a2f1-9c04-4f6e-8a17-2d5e71b0c934
      responses:
        '200':
          description: >-
            The fresh probe result. A probe always stamps `lastCheckedAt`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionHealth'
              examples:
                healthy:
                  summary: A webhook endpoint answered the ping
                  value:
                    state: healthy
                    lastCheckedAt: '2026-08-18T09:14:02.517Z'
                    detail: ping 200
                failedPermanent:
                  summary: Google access was revoked — reconnect required
                  value:
                    state: failed_permanent
                    lastCheckedAt: '2026-08-18T09:15:44.902Z'
                    detail: auth failed (401)
                failed:
                  summary: The endpoint is down — worth retrying
                  value:
                    state: failed
                    lastCheckedAt: '2026-08-18T09:16:10.338Z'
                    detail: unexpected status 502
                unknown:
                  summary: A kind with no probe
                  value:
                    state: unknown
                    lastCheckedAt: '2026-08-18T09:16:51.204Z'
                    detail: no health probe for kind sheets
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No such connection in the caller's project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Connection not found: 00000000-0000-4000-8000-000000000000'
                error: Not Found
                statusCode: 404

  /connections/{connectionId}/calendars:
    get:
      security:
        - portalToken: []
      operationId: listConnectionCalendars
      tags:
        - Integrations & delivery health
      summary: List a Google connection's calendars
      description: >-
        Lists the calendars a connected Google account can see. Use an `id` from
        here as the `calendarId` on an assistant's calendar tool or a campaign's
        calendar setting; with no `calendarId`, bookings go to the literal
        `primary`. Only `owner` and `writer` calendars accept bookings — a
        `reader` calendar like a shared holidays feed will not. The account's
        own calendar is listed under its email address. Portal session token
        only.
      parameters:
        - name: connectionId
          in: path
          required: true
          description: A connection of kind `google_calendar`. Any other kind is a 400.
          schema:
            type: string
            format: uuid
          example: 3b58a2f1-9c04-4e7a-8d61-2f5b8ac41d09
      responses:
        '200':
          description: >-
            The account's calendar list, in Google's order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GoogleCalendarList'
              example:
                calendars:
                  - id: appointments@shifaclinic.pk
                    summary: appointments@shifaclinic.pk
                    primary: true
                    accessRole: owner
                  - id: c_2f8b41xxxx@group.calendar.google.com
                    summary: ڈاکٹر عائشہ — OPD
                    primary: false
                    accessRole: writer
                  - id: en.pk#holiday@group.v.calendar.google.com
                    summary: Holidays in Pakistan
                    primary: false
                    accessRole: reader
        '400':
          description: >-
            Wrong connection kind, or Google refused the token — usually because access was revoked and the account has to reconnect.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                wrongKind:
                  summary: Connection is a different kind
                  value:
                    message: >-
                      Connection 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx is not a
                      Google Calendar connection
                    error: Bad Request
                    statusCode: 400
                revoked:
                  summary: Google access was revoked
                  value:
                    message: Token has been expired or revoked.
                    error: Bad Request
                    statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Sent with a project API key, or with no key at all.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Forbidden resource
                error: Forbidden
                statusCode: 403
        '404':
          description: No such connection in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: 'Connection not found: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
                error: Not Found
                statusCode: 404

  /realtime-assistants/webhook-failures:
    get:
      operationId: listWebhookFailures
      tags:
        - Integrations & delivery health
      summary: List dead-lettered webhook deliveries
      description: |-
        Deliveries the platform gave up on. A row lands here one of three ways — the `payload` variant tells you which: your endpoint rejected the event with a non-retryable 4xx, four attempts all failed, or the connection had no usable URL. Nothing redelivers, so treat this as a repair queue.

        The undelivered event body is not retained — you get the failure metadata, not the payload you missed. To recover the data, re-read it from the API instead: the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes) for `call.completed`/`call.graded`, the campaign's [conversions](/api-reference/campaign-outcomes/list-a-campaigns-conversion-records) or [callbacks](/api-reference/campaign-outcomes/list-a-campaigns-callback-requests) for the rest. Rows are deleted 90 days after abandonment.

        Filter by `assistantId` or `campaignId` to scope the queue. Unlike the [`/connections`](/voice-agents/api-reference/integration/list-connections) routes, this one works with a project API key. An empty page can still carry `nextCursor`; keep paging until it is absent.
      parameters:
        - name: assistantId
          in: query
          required: false
          description: >-
            Restrict to one assistant; omit for the whole organization. An id that doesn't exist returns an empty page, not a 404.
          schema:
            type: string
            format: uuid
          example: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: campaignId
          in: query
          required: false
          description: >-
            Restrict to deliveries fired by one campaign's calls. Combines with `assistantId`.
          schema:
            type: string
            format: uuid
          example: 8f3c1a20-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        - name: limit
          in: query
          required: false
          description: Rows per page. Values above 100 are clamped.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          example: 20
        - name: cursor
          in: query
          required: false
          description: '`nextCursor` from the previous page. Anything else is a 400.'
          schema:
            type: string
      responses:
        '200':
          description: >-
            A page of failures, newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookFailureList'
              examples:
                empty:
                  summary: Nothing has failed
                  value:
                    events: []
                failures:
                  summary: Two abandoned deliveries
                  value:
                    events:
                      - assistantId: ee42ac3b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        campaignId: 8f3c1a20-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        eventId: delivery#7c1de904-xxxx-xxxx-xxxx-xxxxxxxxxxxx#3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        ts: '2026-08-16T09:12:44.108Z'
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        type: call.completed
                        status: failed
                        payload:
                          connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                          statusCode: 404
                          detail: client error 404
                          attempts: 1
                        ttl: 1794647564
                      - assistantId: 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        eventId: delivery#e50b7c33-xxxx-xxxx-xxxx-xxxxxxxxxxxx#3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        ts: '2026-08-15T21:40:03.552Z'
                        organizationId: a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        projectId: c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                        type: call.graded
                        status: failed
                        payload:
                          connectionId: 3b58a2f1-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                          detail: 'exhausted after 4 attempts: FetchError: connect ETIMEDOUT'
                          attempts: 4
                        ttl: 1794606003
                    nextCursor: eyJjYW1wYWlnbklkIjogImFzc2lzdGFudCMwZjRhOTFkMy14eHh4LXh4eHgteHh4eC14eHh4eHh4eHh4eHgiLCAic2siOiAiMjAyNi0wOC0xNVQyMTo0MDowMy41NTJaI2RlbGl2ZXJ5I2U1MGI3YzMzLXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCMzYjU4YTJmMS14eHh4LXh4eHgteHh4eC14eHh4eHh4eHh4eHgiLCAib3JnYW5pemF0aW9uSWQiOiAiYTEyYjdlNzQteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4In0
        '400':
          description: The `cursor` could not be decoded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Invalid pagination cursor
                error: Bad Request
                statusCode: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: Project API key (`sk_api_…`).
    portalToken:
      type: http
      scheme: bearer
      description: >-
        Dashboard session token. [`/connections`](/voice-agents/api-reference/integration/list-connections) routes accept only this — a project API key always gets a 403.
  responses:
    BadRequest:
      description: Validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message:
              - config.agent.instructions should not be empty
              - config.agent.instructions must be a string
            error: Bad Request
            statusCode: 400
    Unauthorized:
      description: A bearer key was sent but is not recognized.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: invalid authorization
            error: Unauthorized
            statusCode: 401
    Forbidden:
      description: No API key sent, or the key lacks permission for this resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Forbidden resource
            error: Forbidden
            statusCode: 403
    NotFound:
      description: No such resource in the API key's project.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: 'Realtime assistant not found: 00000000-0000-4000-8000-000000000000'
            error: Not Found
            statusCode: 404
    ConfigBadRequest:
      description: Validation failed. Nothing was saved.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missingInstructions:
              summary: The agent has no instructions
              value:
                message:
                  - config.agent.instructions should not be empty
                  - config.agent.instructions must be a string
                error: Bad Request
                statusCode: 400
            unknownNoiseModel:
              summary: A noise cancellation model we don't run
              value:
                message:
                  - 'config.noiseCancellation.model must be one of the following values: quail_vf_s, quail_vf_l, quail_l'
                error: Bad Request
                statusCode: 400
    AssistantBusy:
      description: >-
        A save, build or publish holds the assistant. Nothing was changed.
        Retry in a moment.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: The realtime assistant is being modified, try again shortly
            error: Conflict
            statusCode: 409
    VersionNotReady:
      description: >-
        The published version hasn't reached this region yet, usually right
        after a publish. No session or call was started. Retry in a few
        seconds.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Published version 14 of assistant 0f4a91d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx is not readable yet
            statusCode: 503
  schemas:
    Error:
      type: object
      properties:
        message:
          description: >-
            One message per failed field for validation errors, a single string
            otherwise.
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        error:
          type: string
        statusCode:
          type: integer

    CreateAssistantRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        description:
          type: string
        config:
          $ref: '#/components/schemas/AssistantConfig'
        public:
          type: boolean
          default: false
          description: >-
            Allows anyone to open a browser session against this assistant
            without an API key.
      required: [name, config]

    UpdateAssistantRequest:
      type: object
      properties:
        name:
          type: string
          description: An empty string is accepted.
        description:
          type: string
        config:
          $ref: '#/components/schemas/AssistantConfig'
        public:
          type: boolean
          description: >-
            Allows anyone to open a browser session against this assistant
            without an API key. On a published assistant, the change takes
            effect at the next publish.

    Assistant:
      type: object
      properties:
        realtimeAssistantId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        config:
          $ref: '#/components/schemas/AssistantConfig'
        public:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdBy:
          type: string
          description: Who created the assistant — an API key or a portal user.
        updatedBy:
          type: string
        version:
          type: integer
          minimum: 1
          description: >-
            The version this response shows. Every save bumps it, so published
            numbers skip the saves in between.

    AssistantListItem:
      allOf:
        - $ref: '#/components/schemas/Assistant'
        - type: object
          properties:
            aliases:
              $ref: '#/components/schemas/AssistantAliases'

    AssistantAlias:
      type: object
      description: Where an alias points, and who last moved it.
      properties:
        version:
          type: integer
          minimum: 1
        updatedAt:
          type: string
          format: date-time
        updatedBy:
          type: string
          description: The API key or portal user that saved or published.

    AssistantAliases:
      type: object
      properties:
        draft:
          allOf:
            - $ref: '#/components/schemas/AssistantAlias'
          description: The copy you edit. Every save moves it.
        prod:
          allOf:
            - $ref: '#/components/schemas/AssistantAlias'
          description: >-
            What calls run by default. Absent until the first publish, and until
            then calls run the draft.
      required: [draft]

    AssistantPublishState:
      type: object
      properties:
        realtimeAssistantId:
          type: string
          format: uuid
        name:
          type: string
          description: The draft's name.
        description:
          type: string
        aliases:
          $ref: '#/components/schemas/AssistantAliases'

    PublishAssistantRequest:
      type: object
      properties:
        version:
          oneOf:
            - type: string
              const: draft
            - type: integer
              minimum: 1
          description: >-
            `draft` publishes the saved draft under its current `version`
            number. A number rolls back to a version published before. Send it
            as a JSON number, not a string. `prod` is a 400.
        message:
          type: string
          maxLength: 280
          description: >-
            A note for the versions list, like a commit message. Kept once, when
            the version is first published. A rollback's message is not stored.
      required: [version]

    AssistantVersion:
      type: object
      properties:
        version:
          type: integer
        name:
          type: string
          description: The assistant's name as it was published.
        description:
          type: string
        message:
          type: string
          description: The note sent with the first publish of this version. Absent when none was sent.
        publishedAt:
          type: string
          format: date-time
        publishedBy:
          type: string

    AssistantVersionList:
      type: object
      properties:
        versions:
          type: array
          description: Newest first.
          items:
            $ref: '#/components/schemas/AssistantVersion'
        aliases:
          $ref: '#/components/schemas/AssistantAliases'
        nextCursor:
          type: string
          description: Present while more pages exist.

    AssistantConfig:
      type: object
      additionalProperties: true
      description: >-
        Keys beyond those listed are stored and returned unchanged.
      properties:
        agent:
          $ref: '#/components/schemas/AgentConfig'
        grading:
          type: object
          description: >-
            Assistant-level scorecard. Every non-campaign call of this
            assistant — web, inbound, raw API dial — is graded against it;
            campaign calls use the campaign's own scorecard instead. See
            [Scorecards](/voice-agents/assistants/scorecards). A `scorecard`
            that is not an object, or whose points don't total 100, is a 400.
          properties:
            scorecard:
              $ref: '#/components/schemas/Scorecard'
        noiseCancellation:
          type: object
          description: >-
            Cleans the caller's audio before speech-to-text. `enabled` is
            required, and an unknown `model` is a 400.
          properties:
            enabled:
              type: boolean
            model:
              type: string
              enum: [quail_vf_s, quail_vf_l, quail_l]
              default: quail_vf_s
            level:
              type: number
              default: 0.1
              description: >-
                Suppression strength. Stronger settings clipped short Urdu
                backchannels, which is why the default is low.
          required: [enabled]
        backgroundNoise:
          type: object
          description: >-
            Plays ambient room sound under the call. `enabled` is required.
          properties:
            enabled:
              type: boolean
          required: [enabled]
        session:
          $ref: '#/components/schemas/SessionConfig'
        stt:
          type: object
          description: >-
            `default` is the only supported key today (same for `tts`/`llm`).
          properties:
            default:
              $ref: '#/components/schemas/SttConfig'
          required: [default]
        tts:
          type: object
          properties:
            default:
              $ref: '#/components/schemas/TtsConfig'
          required: [default]
        llm:
          type: object
          properties:
            default:
              $ref: '#/components/schemas/LlmConfig'
          required: [default]
      required: [agent]

    AgentConfig:
      type: object
      properties:
        instructions:
          type: string
          minLength: 1
          description: >-
            System prompt. For Urdu assistants, tell the model to reply in Nastaliq script — otherwise it tends to answer in Roman Urdu.
        initialGreeting:
          type: boolean
          default: false
          description: Whether the assistant speaks first instead of waiting for the caller.
        greetingInstructions:
          type: string
          description: >-
            Describe how the agent should greet — it uses the prompt to come up
            with the opening line, so you can say "Say salam and confirm you
            are talking to the right person." Ignored unless `initialGreeting`
            is true.
        tools:
          type: array
          description: >-
            A tool with no `execution` is a client tool: we deliver the call to your browser client over the session, with the tool name as the RPC method. A tool whose `execution` points at a tool endpoint connection goes to that endpoint instead, on phone calls and web sessions alike.
          items:
            $ref: '#/components/schemas/AssistantTool'
      required: [instructions]

    AssistantTool:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        description:
          type: string
          minLength: 1
          description: What the tool does. The model picks the tool from this text.
        parameters:
          type: object
          description: JSON Schema for the tool's arguments.
          properties:
            type:
              type: string
              minLength: 1
            properties:
              type: object
            required:
              type: array
              items:
                type: string
          required: [type, properties]
        timeout:
          type: number
          minimum: 0
          description: >-
            Seconds to wait for a client tool's handler before the assistant
            moves on. Default 10. Webhook tools always get 10 seconds.
        execution:
          type: object
          description: >-
            Where the tool runs. Omit it for a client tool, which your web page
            or mobile app executes over the session. Set `type:
            tool_endpoint` with the id of a tool endpoint you added on the
            [portal](https://upliftai.org/app/calling/webhooks) to have the
            platform POST the call to your server instead. See [Webhook
            tools](/voice-agents/tools/webhook-tools).
          properties:
            type:
              type: string
              enum: [client_rpc, tool_endpoint]
            connectionId:
              type: string
              description: >-
                Required when `type` is `tool_endpoint`. The id of a tool
                endpoint in this project, from the
                [portal](https://upliftai.org/app/calling/webhooks). A webhook's
                id is rejected at call time. Not checked on create: a missing or
                wrong id surfaces as an error result on the call, not a 400.
          required: [type]
      required: [name, description, parameters]

    SessionConfig:
      type: object
      properties:
        ttl:
          type: integer
          minimum: 1
          default: 600
          description: >-
            Lifetime in seconds of each web session's join token. Caps how long a client has to connect, not how long a call may run.
        roomPrefix:
          type: string
          maxLength: 32
          description: >-
            Cosmetic prefix on generated room names. The server appends the session id.

    SttConfig:
      description: Pick the shape matching your provider.
      oneOf:
        - title: Soniox
          type: object
          required: [provider]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [soniox]
            model:
              type: string
              description: '`stt-rt-v4` (default) or `stt-rt-v5`.'
              example: stt-rt-v4
            language:
              type: string
              description: >-
                Bias hint, not a hard filter — language identification stays
                on, so other languages in the audio are still recognized.
                Defaults to `ur`.
              example: ur
            context:
              type: string
              description: >-
                Free-text biasing — names, products, phrases the model should
                expect to hear.
            endpointLatencyAdjustmentLevel:
              type: integer
              minimum: 0
              maximum: 3
              description: >-
                How hard Soniox cuts the wait between the caller stopping and
                the final transcript. Higher is faster, with a small risk of
                ending a turn at a pause. Omit for the Soniox default.
              example: 1
            endpointSensitivity:
              type: number
              minimum: -1
              maximum: 1
              description: >-
                How readily Soniox ends a turn. Higher ends turns sooner.
                Omit for the Soniox default.
            maxEndpointDelayMs:
              type: integer
              minimum: 500
              maximum: 3000
              description: >-
                The longest Soniox waits after the caller stops before it ends
                the turn. Omit for the Soniox default (2000).
            apiKey:
              type: string
              description: >-
                Your own Soniox key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: Deepgram
          type: object
          required: [provider]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [deepgram]
            model:
              type: string
              description: '`nova-3` (default) or `nova-2`.'
              example: nova-3
            language:
              type: string
              description: Defaults to `multi` (multilingual).
              example: multi
            apiKey:
              type: string
              description: >-
                Your own Deepgram key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: OpenAI
          type: object
          required: [provider]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [openai]
            model:
              type: string
              description: Defaults to `whisper-1`.
              example: whisper-1
            language:
              type: string
              description: ISO code. Defaults to `ur`.
              example: ur
            prompt:
              type: string
              description: >-
                Free-text biasing — names, products, phrases the model should
                expect to hear.
            useRealtime:
              type: boolean
              description: Use OpenAI's realtime transcription. Defaults to false.
            apiKey:
              type: string
              description: >-
                Your own OpenAI key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: Groq
          type: object
          required: [provider]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [groq]
            model:
              type: string
              description: >-
                `whisper-large-v3-turbo` (default), `whisper-large-v3`, or
                `distil-whisper-large-v3-en`.
              example: whisper-large-v3-turbo
            language:
              type: string
              description: ISO code. Defaults to `ur`.
              example: ur
            prompt:
              type: string
              description: >-
                Free-text biasing — names, products, phrases the model should
                expect to hear.
            apiKey:
              type: string
              description: >-
                Your own Groq key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: Uplift AI
          type: object
          required: [provider]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [upliftai]
            model:
              type: string
              description: '`large-v3`.'
              example: large-v3
            language:
              type: string
              description: ISO code. Defaults to `ur`.
              example: ur

    TtsConfig:
      description: >-
        Pick the shape matching your provider. A TTS block without a usable
        `voiceId` stores fine and fails at connect time.
      oneOf:
        - title: Uplift AI
          type: object
          required: [provider, voiceId]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [upliftai]
            voiceId:
              type: string
              description: >-
                Pick an id from the [voices gallery](/orator_voices) — 80+
                voices tuned for Urdu and regional languages.
              example: helpdesk-agent
            outputFormat:
              type: string
              description: Defaults to `MP3_22050_32`.
              example: MP3_22050_32
            speed:
              type: number
              minimum: 0.5
              maximum: 2
              default: 1
              description: >-
                How fast the voice speaks. `1` is the normal rate, `0.5` is half
                the speed and `2` is twice as fast. A value outside 0.5 to 2 is
                not rejected: the call speaks at the normal rate.
              example: 1
        - title: OpenAI
          type: object
          required: [provider]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [openai]
            model:
              type: string
              description: Defaults to `gpt-4o-mini-tts`.
              example: gpt-4o-mini-tts
            voiceId:
              type: string
              description: An OpenAI voice name. Defaults to `alloy`.
              example: alloy
            apiKey:
              type: string
              description: >-
                Your own OpenAI key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: Soniox
          type: object
          required: [provider, voiceId]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [soniox]
            model:
              type: string
              description: Defaults to `tts-rt-v1`.
              example: tts-rt-v1
            voiceId:
              type: string
              description: A Soniox voice name.
              example: Maya
            language:
              type: string
              description: ISO code. Defaults to `ur`.
              example: ur
            apiKey:
              type: string
              description: >-
                Your own Soniox key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.

    LlmConfig:
      description: >-
        Pick the shape matching your provider — each takes different knobs, and
        a knob sent to the wrong provider is silently ignored.
      oneOf:
        - title: Google Gemini
          type: object
          required: [provider, model]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [google]
            model:
              type: string
              description: >-
                `gemini-3.5-flash`, `gemini-3.1-flash-lite`, or
                `gemini-2.5-flash`.
              example: gemini-2.5-flash
            thinkingLevel:
              type: string
              enum: [minimal, low, medium, high]
              description: >-
                Gemini 3.x models only. Reasoning depth per response — each
                step up adds latency, so `high` is rarely right for live
                calls. Absent behaves as the voice-tuned `low`.
            thinkingBudget:
              type: integer
              minimum: -1
              maximum: 24576
              description: >-
                Gemini 2.5 models only (not `-lite`). Maximum thinking tokens
                per response: `0` turns thinking off, `-1` lets the model
                decide, `1`–`24576` sets an explicit cap. Absent uses a
                voice-tuned low cap (512).
            apiKey:
              type: string
              description: >-
                Your own Gemini key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: OpenAI
          type: object
          required: [provider, model]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [openai]
            model:
              type: string
              description: >-
                `gpt-5.4-nano`, `gpt-5.4-mini`, or `gpt-4o-mini`.
              example: gpt-5.4-nano
            reasoningEffort:
              type: string
              enum: [none, low, medium, high, xhigh]
              description: >-
                gpt-5.x models. Each step up adds latency per reply. Unset
                runs the model without a reasoning phase (classic
                `temperature: 0`).
            verbosity:
              type: string
              enum: [low, medium, high]
              description: Answer-length control, gpt-5.x models only.
            apiKey:
              type: string
              description: >-
                Your own OpenAI key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: Cerebras
          type: object
          required: [provider, model]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [cerebras]
            model:
              type: string
              description: '`gpt-oss-120b`.'
              example: gpt-oss-120b
            reasoningEffort:
              type: string
              enum: [low, medium, high]
              description: >-
                Supported by the reasoning models (`gpt-oss-120b`). `none` is
                rejected by Cerebras; `verbosity` is not supported on any
                Cerebras model.
            apiKey:
              type: string
              description: >-
                Your own Cerebras key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.
        - title: Groq
          type: object
          required: [provider, model]
          additionalProperties: true
          properties:
            provider:
              type: string
              enum: [groq]
            model:
              type: string
              description: >-
                `openai/gpt-oss-120b` or `openai/gpt-oss-20b`. No reasoning or
                verbosity knobs on this provider.
              example: openai/gpt-oss-120b
            apiKey:
              type: string
              description: >-
                Your own Groq key; omit to bill through Uplift AI. Reads
                return it verbatim, so anyone who can read the assistant can
                see it.

    StartBuildRequest:
      type: object
      properties:
        brief:
          $ref: '#/components/schemas/BuildBrief'
      required: [brief]

    BuildBrief:
      type: object
      properties:
        goal:
          type: string
          description: >-
            What the call is for. Also picks the tool the assistant gets for recording the outcome.
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
        voiceId:
          type: string
          description: >-
            Voice for the built assistant's `tts.default` — ids are in the
            voices gallery.
        gender:
          type: string
          enum: [male, female]
          description: >-
            The agent's own gender. Urdu verb forms are gendered, so the prompt
            is written and graded against it — match it to `voiceId`.
        context:
          type: string
          maxLength: 8000
          description: >-
            The brief in your own words: the business, what the call must
            achieve, the key message, who is being called. Passed to the
            authoring model verbatim, so its detail sets the prompt's quality.
        sampleRows:
          type: array
          maxItems: 20
          description: >-
            Real contact rows, using the column names your campaign will send. Each test persona is grounded in one row, injected the way a live call receives a contact — no call is placed, the rehearsals are text-only.
          items:
            type: object
            additionalProperties:
              type: string
      required: [goal, voiceId, gender, context]

    StartBuildResponse:
      type: object
      properties:
        buildId:
          type: string
          format: uuid
          description: Identifies this build run, for the status poll.
        assistantId:
          type: string
          format: uuid
          description: The assistant being built.
        status:
          type: string
          enum: [started]

    BuildProgress:
      type: object
      properties:
        buildId:
          type: string
          format: uuid
        assistantId:
          type: string
          format: uuid
          description: >-
            Absent while `phase` is `authoring` through `refining`. Keep the id the start-build response gave you.
        status:
          type: string
          enum: [started, running, done, failed]
          description: >-
            `started` lasts a moment, so the first poll usually already reads
            `running`. `done` and `failed` are terminal.
        phase:
          type: string
          enum:
            - authoring
            - generating_personas
            - testing
            - judging
            - refining
            - persisting
            - done
            - failed
          description: >-
            Step the pipeline is on: the prompt is written (`authoring`), test
            personas are generated, each rehearses a text-only call
            (`testing`), the transcripts are graded (`judging`), and anything
            the grader flags drives a rewrite (`refining`) before the next
            round.
        round:
          type: integer
          description: >-
            Which hardening round is running, `1` to `3`; `0` on frames
            outside the loop.
        persona:
          type: string
          description: >-
            The test persona whose rehearsal just finished. Only while
            `testing`.
        transcriptsDone:
          type: integer
          description: Rehearsals finished this round. Only while `testing`.
        transcriptsTotal:
          type: integer
          description: >-
            Rehearsals in this round. Present while `testing` and on the `judging` frame.
        error:
          type: string
          description: >-
            Why the run stopped, when `status` is `failed`. A second run against an assistant that is already building fails here rather than at the request: `A build is already in progress for this assistant`.
        result:
          $ref: '#/components/schemas/BuildOutcome'

    BuildOutcome:
      type: object
      description: The finished build. Present only once `status` is `done`.
      properties:
        assistantId:
          type: string
          format: uuid
        promptVersion:
          type: integer
          description: >-
            Version of the prompt that was saved. It counts up once per rewrite, so anything above `1` means the grader sent it back.
        passed:
          type: boolean
          description: >-
            Whether the last round ended with nothing flagged. `false` means three rounds ran and the grader still had objections. The assistant is saved and callable either way — read the sample transcripts before putting it on a campaign.
        chosen:
          type: object
          description: >-
            The cleanest rehearsal on each path — fewest findings, longest
            conversation breaking ties. Either may be absent.
          properties:
            happy:
              $ref: '#/components/schemas/SampleTranscript'
            negative:
              $ref: '#/components/schemas/SampleTranscript'

    SampleTranscript:
      type: object
      properties:
        label:
          type: string
          description: The persona this rehearsal was run against.
        archetype:
          type: string
          description: The behavior the persona was built to test.
          enum:
            - happy
            - chatty
            - busy
            - suspicious
            - jailbreak
            - opt_out
            - wrong_party
            - mishearing
            - price_pusher
            - polite_no
            - maybe_later
            - angry
        path:
          type: string
          enum: [happy, negative]
        turns:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptTurn'

    TranscriptTurn:
      type: object
      properties:
        speaker:
          type: string
          enum: [agent, caller]
        text:
          type: string
    RefineAssistantRequest:
      type: object
      properties:
        ownerFeedback:
          type: string
          maxLength: 2000
          description: >-
            What to change, in plain language. Carried into every grading round, so a later automated fix cannot quietly undo it.
        brief:
          allOf:
            - $ref: '#/components/schemas/BuildBrief'
          description: >-
            Replaces the brief the original build stored, for this run and every one after — send the whole brief, not just the fields you are changing. Omit to keep the stored brief.
      required: [ownerFeedback]
    CreateSessionRequest:
      type: object
      properties:
        participantName:
          type: string
          minLength: 1
          description: >-
            The LiveKit participant identity the client joins under. Stored on the session record as `participantIdentity`.
        roomName:
          type: string
          description: >-
            Cosmetic prefix for the generated room name, truncated to its first 15 characters. Overrides the assistant's `config.session.roomPrefix`;
            with neither set the prefix is `web`.
        assistantVersion:
          $ref: '#/components/schemas/AssistantVersionSelector'
        variables:
          type: object
          additionalProperties: true
          propertyNames:
            maxLength: 64
          description: >-
            Facts about the person on this session. They reach the model as a
            [caller record](/voice-agents/personalization/variables) and come
            back verbatim on every session read. Same limits as on a
            [call](/api-reference/starting-a-conversation/dispatch-a-call-from-this-assistant).
            **The rendered record rides in the returned `token`, so the browser
            can read it.** Keep secrets out.
        additionalInstructions:
          type: string
          maxLength: 2000
          description: >-
            Appended to the assistant's instructions for this session only.
            **It rides in the returned `token`, so the browser can read it.**
            Keep secrets out.
      required: [participantName]

    SessionToken:
      type: object
      properties:
        token:
          type: string
          description: >-
            LiveKit access token. Hand it to a LiveKit client SDK along with
            `wsUrl`.
        wsUrl:
          type: string
          format: uri
          description: LiveKit WebSocket URL the client connects to.
        roomName:
          type: string
          description: >-
            The room this token opens. Server-generated — treat it as opaque.
    CreateAdhocSessionRequest:
      type: object
      properties:
        participantName:
          type: string
          minLength: 1
          description: Identity your client joins the room under.
        roomName:
          type: string
          description: >-
            Cosmetic room prefix, truncated to its first 15 characters. Overrides `config.session.roomPrefix`; without either, the prefix is `adhoc`.
        config:
          $ref: '#/components/schemas/AssistantConfig'
      required: [participantName, config]
    CreatePublicSessionRequest:
      type: object
      properties:
        participantName:
          type: string
          minLength: 1
          description: Display name of the person joining, as the assistant sees it.
        roomName:
          type: string
          description: >-
            Cosmetic prefix for the generated room — truncated to 15 characters,
            with the session id appended. Overrides the assistant's
            `config.session.roomPrefix`.
      required: [participantName]
    SessionRecord:
      type: object
      description: >-
        One conversation — a browser session or a phone call.
      properties:
        sessionId:
          type: string
          description: >-
            The session id.
        realtimeAssistantId:
          type: string
          format: uuid
          description: >-
            The assistant that ran the session. Still returned after that
            assistant is deleted.
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        channel:
          type: string
          enum: [web, telephony, whatsapp]
        direction:
          type: string
          enum: [inbound, outbound]
          description: Browser sessions are `inbound` by convention — the user opens them.
        state:
          type: string
          enum:
            - created
            - dispatched
            - dialing
            - ringing
            - answered
            - active
            - completed
            - failed
          description: |-
            Each channel walks its own path:

            - **Web session**: `created` → `active` → `completed`
            - **Phone call**: `dispatched` → `dialing` → `ringing` → `answered` → `completed`, or `failed`

            The dial hops come from the carrier, which may skip any of them.
        connected:
          type: boolean
          description: >-
            Whether audio was ever bridged — false for everything up to and including `ringing`.
        outcome:
          type: string
          enum: [busy, no_answer, unreachable, voicemail, silent_pickup, failed]
          description: >-
            Why no conversation happened; present exactly when `connected` is
            false. An in-flight call reads `no_answer` simply because it has not connected yet, so treat it as a verdict only once `state` is `failed`.
        endedBy:
          type: string
          enum: [callee, agent, supervisor, system]
          description: Who hung up.
        toNumber:
          type: string
          description: E.164. Telephony only.
          example: '+923001234567'
        fromNumber:
          type: string
          description: E.164 caller id the callee saw. Telephony only.
          example: '+924232591000'
        calleeName:
          type: string
          description: >-
            Display name of the person called, stamped at dial time from the
            campaign contact.
        roomName:
          type: string
          description: >-
            Server-generated, `{prefix}-{sessionId}`.
        participantIdentity:
          type: string
          description: >-
            Identifies the human on the call: the token identity for a browser session, the SIP participant for a phone call.
        transportProvider:
          type: string
          description: Carrier that placed the call, e.g. `twilio`.
          example: twilio
        variables:
          type: object
          additionalProperties: true
          description: >-
            The values you supplied when the session was created, returned
            verbatim.
        campaignId:
          type: string
          format: uuid
          description: Set on calls a campaign dialed.
        runId:
          type: string
          format: uuid
          description: The campaign run that dialed this call.
        contactId:
          type: string
          description: The campaign contact that was dialed.
        createdAt:
          type: string
          format: date-time
          description: >-
            When the record was created — for an outbound call, when it was
            dispatched, not when it rang.
        dialingAt:
          type: string
          format: date-time
          description: >-
            Carrier accepted the dial request. Reported by the carrier, so it can
            be absent even on a call that connected.
        ringingAt:
          type: string
          format: date-time
          description: >-
            Callee's phone started ringing. Same carrier caveat as `dialingAt`.
        connectedAt:
          type: string
          format: date-time
          description: Audio bridged (SIP answer).
        answeredAt:
          type: string
          format: date-time
          description: >-
            When the callee first spoke. `connectedAt` set while this stays absent is a phantom answer — the line opened and nobody spoke.
        endedAt:
          type: string
          format: date-time
        durationSec:
          type: integer
          description: >-
            Talk time, `answeredAt` to `endedAt`. Absent until the call ends, and
            on calls that never got an `answeredAt`.
        failureReason:
          type: string
          enum:
            - wrong_number
            - busy
            - declined
            - no_answer
            - unreachable
            - voicemail
            - silent_pickup
            - network_error
            - call_failed
          description: >-
            Why the call failed, on `state: failed`. The confusable ones: `no_answer` rang out, `unreachable` never rang (phone off), `silent_pickup` was answered but carried no callee audio, and `network_error` is a carrier or trunk fault rather than anything the callee did.
        failureReasonWithCode:
          type: string
          description: >-
            `failureReason` with the carrier's SIP code appended when there is one, as in `busy:486`.
          example: busy:486
        adhoc:
          type: boolean
          description: The session ran on an inline config rather than a stored assistant.
        public:
          type: boolean
          description: The session was opened without an API key.
        audioUrl:
          type: string
          format: uri
          description: >-
            Recording link, valid one hour. Only when requested with
            `include_audio_url`.
        assistantVersion:
          type: integer
          description: >-
            The assistant version this session ran. Absent on sessions from
            before versions.
        assistantAlias:
          type: string
          enum: [prod, draft]
          description: >-
            The alias the session was launched on. Absent when it was launched
            by an exact version number, and on sessions from before versions.
        summary:
          allOf:
            - $ref: '#/components/schemas/ConversationSummary'
          description: >-
            On list rows only, with `include_summary=true`. Absent, not null,
            when there is none.
    ListSessionsResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/SessionRecord'
        nextCursor:
          type: string
          description: >-
            Present while more pages exist.
    SessionDetail:
      type: object
      properties:
        sessionId:
          type: string
          description: >-
            The session id.
        realtimeAssistantId:
          type: string
          format: uuid
        channel:
          type: string
          enum: [web, telephony, whatsapp]
        direction:
          type: string
          enum: [inbound, outbound]
          description: Web sessions are always `inbound` — the user opens them.
        state:
          type: string
          enum: [created, dispatched, dialing, ringing, answered, active, completed, failed]
          description: >-
            Web runs `created` → `active` → `completed`. Outbound telephony runs `dispatched` → `dialing` → `ringing` → `answered` → `completed` or `failed`; a carrier that reports no dial progress skips straight to `answered`.
        connected:
          type: boolean
          description: >-
            Whether the call ever reached a conversation. False for every
            dial-time state and for `failed`.
        outcome:
          type: string
          enum: [no_answer, busy, voicemail, unreachable, silent_pickup, failed]
          description: >-
            Why the call never reached a conversation. Present only when `connected` is false.
        endedBy:
          type: string
          enum: [callee, agent, supervisor, system]
          description: Who hung up.
        toNumber:
          type: string
          description: E.164. Telephony only.
          example: '+923001234567'
        fromNumber:
          type: string
          description: E.164 caller id the call was placed from. Telephony only.
          example: '+924232591000'
        calleeName:
          type: string
          description: Display name of the person called, from the campaign contact row.
        roomName:
          type: string
        participantIdentity:
          type: string
          description: >-
            The identity the client joined the room under — usually a person's name for web clients, a generated key for dialed calls.
        transportProvider:
          type: string
          example: twilio
        variables:
          type: object
          additionalProperties: true
          description: The values you passed when the call or session was created.
        assistantVersion:
          type: integer
          description: >-
            The assistant version this session ran. Absent on sessions from
            before versions.
        assistantAlias:
          type: string
          enum: [prod, draft]
          description: >-
            The alias the session was launched on. Absent when it was launched
            by an exact version number, and on sessions from before versions.
        campaignId:
          type: string
          format: uuid
          description: Set on calls a campaign dialed.
        runId:
          type: string
          format: uuid
          description: >-
            The campaign run (one launch) this call belongs to. Set with
            `campaignId`.
        contactId:
          type: string
          description: The campaign contact this call was placed to.
        createdAt:
          type: string
          format: date-time
        dialingAt:
          type: string
          format: date-time
          description: >-
            Carrier-reported dial progress. A carrier that never reports a hop
            leaves this and `ringingAt` absent even on a call that connected.
        ringingAt:
          type: string
          format: date-time
        connectedAt:
          type: string
          format: date-time
          description: >-
            When the carrier bridged the audio. `answeredAt` is speech-gated, so `connectedAt` without it is a phantom answer: the line picked up and nobody spoke.
        answeredAt:
          type: string
          format: date-time
          description: When the conversation started.
        endedAt:
          type: string
          format: date-time
        durationSec:
          type: integer
          description: Talk time, `answeredAt` → `endedAt`. Absent when either timestamp is.
        failureReason:
          type: string
          enum:
            - wrong_number
            - busy
            - declined
            - no_answer
            - unreachable
            - voicemail
            - silent_pickup
            - network_error
            - call_failed
          description: >-
            Why a `failed` call failed. Appears alongside `outcome: failed` and
            is the authoritative field of the two.
        failureReasonWithCode:
          type: string
          description: >-
            `failureReason` with the carrier's SIP code appended when there was
            one. Switch on `failureReason`; show this.
          example: busy:486
        transcript:
          type: array
          description: >-
            The conversation in order. Empty on calls that never connected, and
            on a connected call whose report has not been stored yet.
          items:
            $ref: '#/components/schemas/SessionTranscriptTurn'
        toolCalls:
          type: array
          description: Tools the assistant invoked during the call, in order.
          items:
            $ref: '#/components/schemas/SessionToolCall'
        grade:
          $ref: '#/components/schemas/CallScore'
        summary:
          $ref: '#/components/schemas/ConversationSummary'
        conversion:
          $ref: '#/components/schemas/SessionConversion'
        callback:
          $ref: '#/components/schemas/SessionCallback'
        audioUrl:
          type: string
          format: uri
          description: >-
            Presigned link to the recording, valid for one hour. Minted without checking the recording exists, so a call that was never recorded still returns a link that 404s when fetched.

    SessionTranscriptTurn:
      type: object
      properties:
        speaker:
          type: string
          enum: [agent, caller]
        text:
          type: string
        atSec:
          type: integer
          description: >-
            Seconds into the recording this turn began, for lining the
            transcript up with `audioUrl`. Absent when the report carries no
            recording start time.

    SessionToolCall:
      type: object
      properties:
        name:
          type: string
        arguments:
          type: object
          additionalProperties: true
          description: The arguments the model called the tool with.
        result:
          type: string
          description: >-
            What the tool returned, JSON-stringified. Absent when the tool
            returned nothing.
        callId:
          type: string
          description: Pairs the tool call with its result inside the agent's report.
        atSec:
          type: integer
          description: Seconds into the recording, as on transcript turns.

    CallScore:
      type: object
      description: >-
        The rubric grade, written by the post-call pipeline. Criterion titles and points are resolved into it, so rendering a scorecard needs no rubric lookup.
      properties:
        rubricVersionId:
          type: string
          description: The rubric version this call was graded against.
        score:
          type: [integer, 'null']
          description: >-
            Percent of the applicable points earned, 0 to 100. Rules marked N/A
            drop out, so 40 earned of 60 applicable is 67. `null` when no rule
            applied.
        criteria:
          type: array
          items:
            $ref: '#/components/schemas/CallScoreCriterion'
        sentiment:
          type: string
          enum: [happy, neutral, annoyed]
          description: How the caller sounded. Absent on calls graded before this was added.
        gradedAt:
          type: string
          format: date-time

    CallScoreCriterion:
      type: object
      properties:
        ruleId:
          type: string
        title:
          type: string
        points:
          type: integer
          description: The most this rule could earn.
        earned:
          type: integer
          description: '`points` on a pass, 0 on a fail or N/A.'
        passed:
          type: [boolean, 'null']
          description: >-
            `null` when the rule did not apply to this call. Read `status` for
            the three-way answer.
        status:
          type: string
          enum: [pass, fail, not_applicable]
          description: Absent on calls graded before N/A existed.
        reason:
          type: string
          maxLength: 500
          description: Why the grader answered this way. Always present on `not_applicable`.
        evidence:
          type: object
          description: The moment in the call the answer rests on. Can be absent.
          properties:
            atSec:
              type: integer
              description: Seconds into the recording, as on transcript turns.
            quote:
              type: string
              maxLength: 500
              description: Copied from the transcript.
            speaker:
              type: string
              enum: [agent, contact]

    ConversationSummaryFact:
      type: object
      properties:
        label:
          type: string
          description: One to three words, Title Case, e.g. `Decision`, `Delivery Address`, `Complaint Ref`.
          example: Decision
        value:
          type: string
          description: Up to fifteen words, English, Latin script.
          example: Will try one bottle on the next corn crop
      required: [label, value]

    ConversationSummary:
      type: object
      description: >-
        Call summary: how it went, in a headline and a few bullets, plus the
        facts that came up. In English. Not available for adhoc sessions.
      properties:
        headline:
          type: string
          description: One sentence, end state first.
          example: Farmer agreed to try one bottle of Cruiser on the next corn crop and asked for an expert callback.
        bullets:
          type: array
          description: Usually three to five, in call order. Events, not values.
          items:
            type: string
          example:
            - Agent confirmed the farmer and introduced Cruiser seed treatment for corn
            - Farmer agreed to try one bottle on the next sowing
            - Farmer asked about rice pollination temperature and wanted an expert to call
        facts:
          type: array
          description: >-
            One per distinct fact, in display order. The first is the
            person's Decision or the Next Step when one exists.
          items:
            $ref: '#/components/schemas/ConversationSummaryFact'
        language:
          type: string
          enum: [en]
        promptVersion:
          type: string
          example: call-summary-v1
        model:
          type: string
          description: The model that wrote it.
          example: gpt-5.4-2026-03-05
        generatedAt:
          type: string
          format: date-time
      required: [headline, bullets, facts, language, promptVersion, model, generatedAt]

    SessionConversion:
      type: object
      description: >-
        The goal outcome recorded for this call. Absent when the call produced
        none.
      properties:
        goalKind:
          type: string
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
        payload:
          $ref: '#/components/schemas/ConversionPayload'
        slotIso:
          type: string
          format: date-time
          description: The time that was booked, for goals that book one.
        createdAt:
          type: string
          format: date-time

    SessionCallback:
      type: object
      description: >-
        The contact asked to be called again on this call. Absent when they did not.
      properties:
        kind:
          $ref: '#/components/schemas/CallbackKind'
        reason:
          type: string
          description: One line on why.
        status:
          $ref: '#/components/schemas/CallbackStatus'
        callbackAfter:
          $ref: '#/components/schemas/CallbackAfter'
        requestedText:
          type: string
          description: The contact's words that asked for the callback.
        timezone:
          type: string
          description: The IANA zone `callbackAfter` was resolved in.
        redial:
          $ref: '#/components/schemas/CallbackRedial'
        timeline:
          type: array
          description: What happened to the callback, oldest first. Absent on older callbacks.
          items:
            $ref: '#/components/schemas/CallbackEvent'
        createdAt:
          type: string
          format: date-time
    CreateCallRequest:
      type: object
      properties:
        assistantId:
          type: string
          format: uuid
          description: The assistant that runs the call.
        to:
          type: string
          pattern: '^(\+[1-9]\d{1,14}|0\d{8,14})$'
          description: >-
            Callee number, E.164 or Pakistani local format (`03001234567`, stored as `+92…`). A well-formed number that is not dialable still fails with a 400.
          example: '+923001234567'
        from:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
          description: >-
            Caller ID to dial from. Omit to use your organization's dedicated
            number, or the shared pool if it has none.
          example: '+924238900100'
        variables:
          type: object
          additionalProperties: true
          propertyNames:
            maxLength: 64
          description: >-
            Per-call data kept on the call record and returned by the session
            reads. It is not interpolated into the prompt; anything the assistant must say belongs in `additionalInstructions`. Keys up to 64 characters, and the serialized object must stay under 3000 characters.
        additionalInstructions:
          type: string
          maxLength: 2000
          description: Appended to the assistant's system prompt, for this call only.
      required: [assistantId, to]

    CreateCallResponse:
      type: object
      properties:
        callId:
          type: string
          description: >-
            Also the `sessionId` for every later read of this call.
        status:
          type: string
          enum: [dispatched]

    SimpleError:
      type: object
      description: >-
        Failures raised after the body validates carry no `error` name, just a message and status.
      properties:
        message:
          type: string
        statusCode:
          type: integer

    CallConflict:
      type: object
      properties:
        error:
          type: string
          enum: [duplicate_in_flight, number_busy]
          description: >-
            `duplicate_in_flight` — this `Idempotency-Key` already placed a call.
            `number_busy` — another live call already has that callee number, whoever
            started it.
        callId:
          type: string
          description: >-
            The id this request resolved to: the existing call on `duplicate_in_flight`, the refused attempt on `number_busy`.
    AssistantCallRequest:
      type: object
      properties:
        toNumber:
          type: string
          pattern: '^(\+[1-9]\d{1,14}|0\d{8,14})$'
          description: >-
            Number to dial, either E.164 (`+923001234567`) or a local
            leading-zero number (`03001234567`).
          example: '+923001234567'
        fromNumber:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
          description: >-
            Caller ID to dial from, E.164. Only the format is checked, so a number your organization doesn't own dispatches and then fails at the carrier. Omit to let the platform allocate one.
          example: '+924232175000'
        variables:
          type: object
          additionalProperties: true
          propertyNames:
            maxLength: 64
          description: >-
            Facts about this caller, like `crop: cotton`. They reach the model
            as a [caller record](/voice-agents/personalization/variables) after
            the prompt, and come back verbatim on every session read. String,
            number and boolean values go to the model. A nested object stays on
            the record only. Keys up to 64 characters, and the serialized object
            must stay under 3000 characters.
        additionalInstructions:
          type: string
          maxLength: 2000
          description: >-
            Appended to the assistant's instructions for this call only.
        assistantVersion:
          $ref: '#/components/schemas/AssistantVersionSelector'
      required: [toNumber]

    AssistantVersionSelector:
      oneOf:
        - type: string
          enum: [prod, draft]
        - type: integer
          minimum: 1
      default: prod
      description: >-
        Which version of the assistant runs. `prod` is the published version,
        or the draft before the first publish. `draft` tries unpublished edits
        on a real conversation. A number runs that exact published version,
        and one never published is a 404. Send a number as a JSON number, not
        a string.
      example: draft

    AssistantCallResponse:
      type: object
      properties:
        callId:
          type: string
          description: >-
            Id of this call, and the `sessionId` in every session and call record.
        externalRef:
          type: string
          description: Telephony-provider dispatch reference, useful when raising support tickets.
        status:
          type: string
          const: dispatched
    SimulateRequest:
      type: object
      properties:
        persona:
          $ref: '#/components/schemas/SimulatePersona'
        context:
          type: string
          maxLength: 8000
          description: >-
            What the person picking up knows about who is ringing them and why. Overrides the `context` in the assistant's stored brief; with neither, they go in cold.
        variables:
          type: object
          additionalProperties:
            type: string
          description: >-
            The contact record for this one simulated call, handed to the agent under test as its caller record. Falls back to the first `sampleRows` entry of the stored brief. Keys are at most 64 characters, and the whole object under 3000 characters of JSON.
      required: [persona]

    SimulatePersona:
      type: object
      description: >-
        Who picks up. Kept separate from `context` and `variables` so one persona can be re-run across scenarios and contacts.
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 120
          description: Short name for the persona, echoed back on the transcript.
        behavior:
          type: string
          minLength: 1
          maxLength: 1000
          description: >-
            How they behave on the call, in plain language. Goes into the persona model's prompt verbatim, so write instructions rather than adjectives.
        path:
          type: string
          enum: [happy, negative]
          default: negative
          description: >-
            How the conversation was meant to go, recorded on the transcript. `behavior` is what actually drives the caller.
      required: [label, behavior]
    AssistantBuildRecord:
      type: object
      description: >-
        `passed`, `agentName`, `businessName` and `sampleTranscripts` land only
        when a build finishes; the rest is written the moment it starts.
      properties:
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        assistantId:
          type: string
          format: uuid
        status:
          type: string
          enum: [building, ready, failed]
          description: >-
            `building` while the first run is in flight, then `ready` on success or `failed` on a crash — the reason lands in `error`. A refine keeps the previous value until its run ends.
        promptVersion:
          type: integer
          description: >-
            Counts up once per rewrite, so anything above `1` means the grader sent the prompt back.
        passed:
          type: boolean
          description: >-
            Whether the final round ended with nothing flagged. `false` means the
            rounds ran out with the grader still objecting: the assistant is
            callable either way, so read the sample transcripts first.
        agentName:
          type: string
          description: The name the assistant introduces itself by, chosen during authoring.
        businessName:
          type: string
          description: >-
            The business it says it is calling for, taken from the brief.
        brief:
          allOf:
            - $ref: '#/components/schemas/BuildBrief'
          description: >-
            The brief this build ran from, echoed with the ids it was stored
            under. A refine that sends a new brief replaces it.
        sampleTranscripts:
          type: object
          description: >-
            The cleanest rehearsal on each path — fewest findings, longest
            conversation breaking ties. Either path may be missing.
          properties:
            happy:
              $ref: '#/components/schemas/SampleTranscript'
            negative:
              $ref: '#/components/schemas/SampleTranscript'
        buildId:
          type: string
          format: uuid
          description: The run that last wrote this record — what the status poll's durable fallback matches on.
        error:
          type: string
          description: Why the last run failed. Only present when `status` is `failed`.
        updatedAt:
          type: string
          format: date-time
          description: When the record was last written — build start or finish.
    WebhookFailureList:
      type: object
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookFailureEvent'
        nextCursor:
          type: string
          description: >-
            Pass as `cursor` for the next page. A page can come back short or empty and still return a cursor, so keep paging until this field is absent.

    WebhookFailureEvent:
      type: object
      description: One event that could not be delivered to one connection.
      properties:
        assistantId:
          type: string
          format: uuid
        campaignId:
          type: string
          format: uuid
          description: Absent when the call was not part of a campaign.
        eventId:
          type: string
          description: >-
            `delivery#<event id>#<connection id>`.
        ts:
          type: string
          format: date-time
          description: When the delivery was abandoned, not when the event occurred.
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        type:
          type: string
          description: |-
            The event that failed to deliver:

            - `call.completed` — a call ended
            - `call.graded` — post-call analysis finished for a call
            - `conversion.created` — an outcome was recorded
            - `callback.opened` — a contact asked to be called again
            - `callback.failed` — a campaign's redial for a callback ended without reaching the contact
          enum:
            - call.completed
            - call.graded
            - conversion.created
            - callback.opened
            - callback.failed
        status:
          type: string
          enum: [failed]
          description: Always `failed`.
        payload:
          description: >-
            Failure metadata only — the undelivered event body is not retained.
            The variant tells you why delivery stopped.
          oneOf:
            - title: Rejected by your endpoint
              type: object
              description: >-
                Your endpoint answered with a non-retryable 4xx — fix the
                endpoint, not the platform.
              properties:
                connectionId:
                  type: string
                  format: uuid
                  description: >-
                    The webhook connection the delivery was posted to — readable at [`/connections/{connectionId}`](/voice-agents/api-reference/integration/get-a-connection).
                statusCode:
                  type: integer
                  description: The 4xx your endpoint returned.
                detail:
                  type: string
                  description: Free text, not a stable code — e.g. `client error 404`.
                attempts:
                  type: integer
                  description: '`1` — a non-retryable response ends delivery on the first try.'
            - title: Retries exhausted
              type: object
              description: >-
                Timeouts, 5xx, or network failures on every attempt.
              properties:
                connectionId:
                  type: string
                  format: uuid
                  description: >-
                    The webhook connection the delivery was posted to — readable at [`/connections/{connectionId}`](/voice-agents/api-reference/integration/get-a-connection).
                statusCode:
                  type: integer
                  description: >-
                    The last HTTP status received. Absent when no response ever
                    arrived — timeout, DNS or TLS failure.
                detail:
                  type: string
                  description: 'Free text — e.g. `exhausted after 4 attempts: …`.'
                attempts:
                  type: integer
                  description: '`4` — the retry ceiling.'
            - title: Never deliverable
              type: object
              description: >-
                The connection had no usable URL — none configured, or one the
                platform refuses (non-https, private address).
              properties:
                connectionId:
                  type: string
                  format: uuid
                  description: >-
                    The webhook connection — fix its `config.url` via [update](/voice-agents/api-reference/integration/update-a-connection).
                detail:
                  type: string
                  description: e.g. `connection has no url configured`.
                attempts:
                  type: integer
                  description: '`1` — nothing was ever sent.'
        ttl:
          type: integer
          description: Unix seconds at which this row is deleted — 90 days after `ts`.
    Campaign:
      type: object
      properties:
        campaignId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 120
        state:
          type: string
          enum:
            - draft
            - scheduled
            - launching
            - launch_failed
            - running
            - paused
            - draining
            - completed
          description: |-
            Where the campaign is in its lifecycle:

            - `draft` — created, nothing dials
            - `scheduled` — the platform launches it at `config.schedule.scheduledFor`
            - `launching` — the contact list is being materialized into a run; transient
            - `launch_failed` — materialization failed; `lastError` says which phase, and launching again retries it
            - `running` — the dialer is placing calls
            - `paused` — no new dials; calls already in flight run to their end
            - `draining` — closed to new work, waiting only on in-flight calls
            - `completed` — terminal
        mode:
          type: string
          enum: [bounded, continuous]
          description: >-
            Fixed at create. `bounded` snapshots the contact list at launch and
            completes itself once the queue empties; `continuous` keeps accepting
            appended contacts and completes only after a close. Absent on older campaigns; read that as `bounded`.
        config:
          $ref: '#/components/schemas/CampaignConfig'
        closedAt:
          type: string
          format: date-time
          description: >-
            When the campaign was closed to new work. Continuous campaigns only; closing does not change `state`, so expect `running` until the drain begins.
        currentRunId:
          type: string
          format: uuid
          description: >-
            The single run created at first launch; a retried launch reuses it.
        runStartedAt:
          type: string
          format: date-time
          description: First launch. Unchanged by pauses, resumes and launch retries.
        lastError:
          type: object
          description: Why the last launch failed. Cleared by a launch that gets further.
          properties:
            phase:
              type: string
              description: The launch step that threw, e.g. `materialize`.
            message:
              type: string
            at:
              type: string
              format: date-time
        statusMessage:
          type: string
          description: >-
            Set when the platform acts on its own, and says why. Expect
            `Paused: out of credits`, or `Paused: assistant version not found`
            when the campaign's assistant was deleted. The assistant can't be
            changed after launch, so a resume won't fix that one. Cleared on
            resume.
          example: 'Paused: out of credits'
        hasGradedCall:
          type: boolean
          description: >-
            True once any call has been graded, after which `config.scorecard` is frozen. Absent until then.
        trigger:
          type: object
          description: 'How the campaign takes work. Always `{"kind": "list"}` today.'
          properties:
            kind:
              type: string
              enum: [list]
        steps:
          type: array
          description: What each contact goes through. Always `["call"]` today.
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        version:
          type: integer
          description: >-
            Increments on every accepted write — compare it across reads to tell a
            real change from an unchanged poll.

    CampaignConfig:
      type: object
      additionalProperties: true
      description: >-
        Returned exactly as stored — a section never set is absent rather than
        filled in with defaults.
      properties:
        goal:
          type: string
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
          description: >-
            What the calls are for. Picks the outcome-recording tool handed to the
            assistant.
        assistant:
          type: object
          description: The assistant that dials. Frozen once the campaign leaves `draft`.
          properties:
            assistantId:
              type: string
              format: uuid
        assistantVersion:
          oneOf:
            - type: string
              enum: [prod, draft]
            - type: integer
              minimum: 1
          description: >-
            Which version of the assistant the calls run. Absent means `prod`,
            so a publish reaches the next dial. A number pins a published
            version, needs `assistant` set, and is a 400 if that version was
            never published. Editable while the campaign runs.
        contacts:
          type: object
          description: >-
            The contact list this campaign dials, created via [`/contact-lists`](/api-reference/contacts/create-a-contact-list).
            All the ways contacts get into a campaign:
            [Contacts](/voice-agents/campaigns/contacts).
          properties:
            listId:
              type: string
              format: uuid
        schedule:
          type: object
          properties:
            scheduledFor:
              type: string
              format: date-time
              description: When a scheduled launch fires. Set by the schedule endpoint.
            callingWindow:
              $ref: '#/components/schemas/CallingWindow'
        dialing:
          type: object
          properties:
            lines:
              type: integer
              description: >-
                Requested concurrent calls. Unset defaults to 1, and an explicit value below 1 fails the launch. Your account's own limit caps it: 1 call at a time by default, shared with direct calls and inbound, so a larger number only dials faster once that limit is raised. See [Phone numbers & concurrency](/voice-agents/calling/concurrency).
            fromNumber:
              type: string
              description: Caller id the calls are placed from.
              example: '+924232567890'
            redialCallbacks:
              type: boolean
              default: true
              description: >-
                When a contact asks to be called later, requeue them for that time, up to 3 dial cycles per contact. `false` ends the contact as before, and the callback stays `open` for a person. It applies to new callbacks only: contacts already requeued are still called.
            retryPolicy:
              $ref: '#/components/schemas/RetryPolicy'
        calendar:
          type: object
          description: >-
            Booking calendar for `appointment` campaigns. Supplying `connectionId`, a Google Calendar connection from the [portal](https://upliftai.org/app/calling/webhooks), adds the booking tools to the call.
          properties:
            connectionId:
              type: string
            calendarId:
              type: string
        scorecard:
          $ref: '#/components/schemas/Scorecard'
        tools:
          type: object
          description: Extra tools stamped onto the assistant for this campaign's calls.
        delivery:
          type: object
          description: Where call outcomes are delivered.
        economics:
          type: object
          description: >-
            Per-conversion values that turn campaign stats counts into money.
          additionalProperties:
            type: number
    CreateCampaignRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        mode:
          type: string
          enum: [bounded, continuous]
          default: bounded
          description: >-
            Fixed at creation. `bounded` snapshots the contact list at launch and completes once the queue empties; `continuous` keeps accepting appended contacts and ends only when you close it.
        config:
          $ref: '#/components/schemas/CampaignConfig'
      required: [name]

    CallingWindow:
      type: object
      description: >-
        Local hours during which this campaign may dial; omit it and the campaign dials around the clock. Retries falling outside the window wait until it next opens. There is no per-weekday control. The shape is not validated — a malformed window misbehaves at dial time instead of erroring here.
      properties:
        from:
          type: string
          description: 24-hour local time, `HH:MM`.
          example: '10:00'
        to:
          type: string
          description: >-
            24-hour local time, `HH:MM`. A `to` earlier than `from` wraps past midnight.
          example: '19:00'
        tz:
          type: string
          description: IANA timezone.
          example: Asia/Karachi
      required: [from, to, tz]

    RetryPolicy:
      type: object
      description: >-
        Applied all or nothing. Unless `maxAttempts`, `retryOn` and `backoffMinutes` are all present and usable, every field falls back to its default below — a partial policy is ignored, not merged.
      properties:
        maxAttempts:
          type: integer
          minimum: 1
          default: 3
          description: Dials per contact, the first attempt included.
        retryOn:
          type: array
          default: [no_answer, busy, voicemail, failed, silent_pickup, unreachable]
          description: >-
            Outcomes worth dialing again. `booked`, `callback`, `not_interested` and `wrong_number` never retry, even if listed here. A contact who asked to be called later is requeued for that time instead, unless `dialing.redialCallbacks` is `false`. `unreachable` (phone off or out of coverage) is usually brief here, so the default retries it.
          items:
            type: string
            enum:
              - no_answer
              - busy
              - voicemail
              - failed
              - silent_pickup
              - unreachable
              - wrong_person
              - booked
              - callback
              - not_interested
              - wrong_number
        backoffMinutes:
          type: array
          default: [10, 60]
          description: >-
            Minutes to wait before each following attempt; the last value repeats
            for any attempts beyond the list.
          items:
            type: integer
            minimum: 0
      required: [maxAttempts, retryOn, backoffMinutes]

    Scorecard:
      type: object
      description: >-
        The rubric every call on this campaign is graded against. It freezes once
        any call has been graded, so revise it before launching.
      properties:
        rubricVersionId:
          type: string
          minLength: 1
        rules:
          type: array
          minItems: 1
          description: >-
            Rule ids must be unique and their `points` must total exactly 100.
          items:
            $ref: '#/components/schemas/ScorecardRule'
      required: [rubricVersionId, rules]

    ScorecardRule:
      type: object
      properties:
        ruleId:
          type: string
          minLength: 1
        title:
          type: string
          minLength: 1
        instruction:
          type: string
          minLength: 1
          description: What the grader looks for in the transcript to award this rule.
        scoringType:
          type: string
          enum: [binary]
          description: Each rule is earned in full or not at all.
        points:
          type: integer
          minimum: 1
        evidenceRequired:
          type: boolean
          description: >-
            When true, the grader must cite a transcript quote to award the rule.
      required: [ruleId, title, instruction, scoringType, points]
    UpdateCampaignRequest:
      type: object
      description: >-
        Both keys are optional and an empty body is a no-op. Unknown keys are dropped silently rather than rejected — `mode` in particular is fixed at create.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: Trimmed before storage; whitespace-only is rejected.
        config:
          $ref: '#/components/schemas/CampaignConfig'
    ScheduleCampaignRequest:
      type: object
      properties:
        scheduledFor:
          type: string
          format: date-time
          description: >-
            When to launch. Date-only (`2026-12-24`) and offset-less (`2026-12-24T09:30:00`) forms are also accepted, so send `Z` or an explicit offset to keep the time unambiguous. A time already in the past is not rejected — it launches on the next sweep.
      required: [scheduledFor]
    CampaignLaunchResult:
      type: object
      properties:
        runId:
          type: string
          format: uuid
          description: >-
            The run this launch created, and the id its calls and results are filed under. A campaign has one run for its lifetime, so retrying a failed launch reuses this id.
        enrolled:
          type: integer
          description: >-
            Contacts copied into the dial queue by this attempt. A retry counts only the rows it adds, so it can read low or `0` against an already-populated queue. A continuous campaign launched before it has a list also reads `0`; those contacts arrive later via [`/campaigns/{id}/contacts`](/api-reference/campaigns/append-contacts-to-a-campaign).
    PreviewUploadRequest:
      type: object
      properties:
        mapping:
          allOf:
            - $ref: '#/components/schemas/ContactColumnMapping'
          description: >-
            Omit it to re-run auto-detection — the same preview the upload
            returned.

    ContactColumnMapping:
      type: object
      description: >-
        Column overrides. Whatever you leave out is auto-detected from the headers.
      properties:
        nameColumn:
          type: string
          description: >-
            Header name (case-insensitive) or A1 column letter. An explicit value never falls back to auto-detection — if it matches no column, names read as empty and the preview reports it in `mappingIssues`.
          example: Name
        phoneColumn:
          type: string
          description: Header name or A1 column letter, resolved like `nameColumn`.
          example: B
        attributeColumns:
          type: array
          items:
            type: string
          description: >-
            Columns to carry as per-contact attributes, usable as personalization
            variables. Exact header names only — no A1 letters. Omit to keep every
            column that is not the name or phone column; send `[]` to keep none.
          example: [City, Appointment]

    ContactPreview:
      type: object
      properties:
        summary:
          $ref: '#/components/schemas/ContactImportSummary'
        detectedMapping:
          allOf:
            - $ref: '#/components/schemas/ContactColumnMapping'
          description: >-
            The mapping applied. Overrides you sent are resolved to header names — an A1 letter comes back as its header — and the rest is auto-detected.
        sample:
          type: array
          items:
            $ref: '#/components/schemas/PreviewContact'
          description: First 10 contacts that would be called, after normalization.
        total:
          type: integer
          description: >-
            Data rows read, excluding the header. `summary.willBeCalled` and the `summary.excluded` counts add up to this.
        columns:
          type: array
          items:
            type: string
          description: Headers as parsed, in file order — the choices for a mapping override.
        mappingIssues:
          type: array
          items:
            type: string
            enum: [phoneColumnNotFound, nameColumnNotFound]
          description: >-
            Columns that resolved to nothing. Check this first when everything is excluded — an unmapped phone column makes every number look invalid.

    ContactImportSummary:
      type: object
      properties:
        willBeCalled:
          type: integer
        excluded:
          type: object
          description: Row counts per reason for exclusion.
          properties:
            invalidPhone:
              type: integer
              description: Missing, or not dialable as an E.164 number.
            missingName:
              type: integer
            duplicate:
              type: integer
              description: A number already counted earlier in the file.
            dnc:
              type: integer
              description: Always `0` — no do-not-call suppression is applied here.
            recentlyCalled:
              type: integer
              description: Always `0` — no recent-call suppression is applied here.
        errors:
          type: array
          description: Per-row detail for excluded rows, capped at the first 20.
          items:
            type: object
            properties:
              row:
                type: integer
                description: Row number in the file, counting the header — so data starts at 2.
              issue:
                type: string
                enum: [invalidPhone, missingName, duplicate, dnc, recentlyCalled]
              value:
                type: string
                description: The offending cell, empty when the column was absent.

    PreviewContact:
      type: object
      description: One contact as it would be dialed.
      properties:
        name:
          type: string
        phone:
          type: string
          description: >-
            Dialable number, cleaned up but kept in the format the file used —
            a Pakistani national `03…` stays `03…`. Two spellings of the same number still count as one.
        attributes:
          type: object
          additionalProperties:
            type: string
          description: Attribute columns for this row; empty cells are dropped.
    ContactUploadPreview:
      allOf:
        - $ref: '#/components/schemas/ContactPreview'
        - type: object
          properties:
            uploadId:
              type: string
              format: uuid
              description: >-
                Handle for the stored rows. Uploads are scoped to the API key's project and cannot be deleted.
    CreateContactListRequest:
      oneOf:
        - title: Uploaded file
          type: object
          required: [name, kind, uploadId]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
            kind:
              type: string
              enum: [csv_upload]
            uploadId:
              type: string
              format: uuid
              description: >-
                An upload from [`/contact-lists/uploads`](/api-reference/contacts/upload-a-contact-file), reusable for more than one list.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: >-
                Overrides the columns detected when the file was uploaded.
        - title: Inline CSV or pasted rows
          type: object
          required: [name, kind, data]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
            kind:
              type: string
              enum: [csv_upload, paste]
              description: >-
                `csv_upload` for CSV text with a header row; `paste` for one-line-per-contact rows.
            data:
              type: string
              description: >-
                The rows themselves: CSV text for `csv_upload`, or one
                `نام, +923001234567` per line for `paste`.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: Overrides column auto-detection.
        - title: Google Sheet (one-time pull)
          type: object
          required: [name, kind, ref]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
            kind:
              type: string
              enum: [google_sheets]
            ref:
              type: string
              description: >-
                Spreadsheet to pull, as `{spreadsheetId}` or `{spreadsheetId}!{tab}`. The rows are copied once, now — to re-pull the sheet at every launch, [register a contact source](/api-reference/google-sheet-sources/register-a-google-sheet-as-a-source) instead.
            connectionId:
              type: string
              format: uuid
              description: >-
                Reads the sheet through a Google connection instead of the public
                path.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: Overrides column auto-detection.

    ContactList:
      type: object
      properties:
        listId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
        sourceKind:
          type: string
          enum: [csv_upload, paste, google_sheets, customer_api]
        sourceRef:
          type: string
          description: >-
            Present only when the import supplied an `uploadId` or a sheet `ref` — inline rows have none, even when labelled `csv_upload`.
        summary:
          $ref: '#/components/schemas/ImportSummary'
        contactCount:
          type: integer
          description: Contacts stored — the same number as `summary.willBeCalled`.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          description: Equal to `createdAt`, since a list is never edited.
        version:
          type: integer
          description: Always `1`.

    ImportSummary:
      type: object
      description: What the import kept and dropped.
      properties:
        willBeCalled:
          type: integer
          description: Rows that became contacts.
        excluded:
          type: object
          description: >-
            Dropped rows by reason. `dnc` and `recentlyCalled` are always `0` — those checks do not run yet.
          properties:
            invalidPhone:
              type: integer
            missingName:
              type: integer
            duplicate:
              type: integer
              description: >-
                Same number seen twice. Numbers are compared as E.164, so `03001234567` and `+923001234567` collapse to one contact, keeping the first spelling.
            dnc:
              type: integer
            recentlyCalled:
              type: integer
        errors:
          type: array
          description: The dropped rows, first 20 only.
          items:
            type: object
            properties:
              row:
                type: integer
                description: Line in the source file, counting the header as line 1.
              issue:
                type: string
                enum: [invalidPhone, missingName, duplicate, dnc, recentlyCalled]
              value:
                type: string
                description: >-
                  The row's phone number: raw source text for `invalidPhone` and `missingName`, normalized for `duplicate`.
    ContactListPage:
      type: object
      properties:
        lists:
          type: array
          items:
            $ref: '#/components/schemas/ContactList'
        nextCursor:
          type: string
          description: >-
            Absent on the last page. Present means fetch again with it as
            `cursor`.
    ContactPage:
      type: object
      properties:
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/Contact'
        nextCursor:
          type: string
          description: >-
            Pass back as `cursor` to read the next page. Absent on the last
            page.

    Contact:
      type: object
      properties:
        listId:
          type: string
          format: uuid
        contactId:
          type: string
          description: >-
            The contact's phone number. Contacts are deduplicated on it within a list.
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
          description: Who the assistant addresses on the call.
        phone:
          type: string
          description: >-
            The number to dial, same value as `contactId`. Kept in the format it was imported in — `+923001234567` and `03001234567` are both stored as given.
        attributes:
          type: object
          additionalProperties:
            type: string
          description: >-
            The imported row's remaining columns, under their original headers. On a campaign dial they are appended to the assistant's instructions as a contact record — there is no `{{placeholder}}` substitution.
    AppendContactsRequest:
      description: >-
        Send exactly one variant — none, or fields from two, is a 400.
      oneOf:
        - title: Inline rows
          type: object
          required: [contacts]
          properties:
            contacts:
              type: array
              minItems: 1
              maxItems: 1000
              description: Up to 1000 rows per request.
              items:
                $ref: '#/components/schemas/AppendContact'
        - title: Uploaded file
          type: object
          required: [uploadId]
          properties:
            uploadId:
              type: string
              minLength: 1
              description: >-
                The `uploadId` returned by [`/contact-lists/uploads`](/api-reference/contacts/upload-a-contact-file). Parsed, mapped and validated like a contact-list import, up to 5000 rows. Rows take their identity from an id column (`external_ref`, `order_id` and similar) when the file has one, from the phone number otherwise.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: >-
                Names the columns when auto-detection would get them wrong.
        - title: Connected sheet
          type: object
          required: [sheet]
          properties:
            sheet:
              allOf:
                - $ref: '#/components/schemas/AppendSheetSource'
              description: >-
                Read live at append time, up to 5000 rows, with the same parsing
                and identity rules as an uploaded file.
            mapping:
              allOf:
                - $ref: '#/components/schemas/ContactColumnMapping'
              description: >-
                Names the columns when auto-detection would get them wrong.

    AppendContact:
      type: object
      description: >-
        No field is required, but a new contact without a usable phone or a
        name comes back `skipped`.
      properties:
        externalRef:
          type: string
          pattern: '^[A-Za-z0-9_.:-]{1,64}$'
          description: >-
            Your own id for this job — an order, ticket or lead number. It becomes the contact's identity, so sending it again reaches the same contact rather than adding a second one, and it travels verbatim into call ids and webhook events. A value outside the pattern gives `skipped` / `invalidExternalRef`; omit it and the row always enrolls as a new contact.
          example: ord-48812
        name:
          type: string
          description: >-
            Spoken aloud during the call. An Urdu name written in Roman letters
            is read out as English, so send Nastaliq. A new contact needs it:
            blank or missing gives `skipped` / `missingName`. Leave it out when
            your `externalRef` names a contact we already have, and the stored
            name is kept.
        phone:
          type: string
          description: >-
            Pakistani national (`03001234567`) or E.164 (`+923001234567`), stored
            in the format you send. Anything unparseable gives `skipped` /
            `invalidPhone`.
        attributes:
          type: object
          additionalProperties:
            type: string
          description: >-
            Row data the assistant is given for the call. Capped at 1500 bytes of JSON — past the cap, values are trimmed and later keys dropped, so put what matters first.
            When the row re-arms a finished contact, sending `attributes`
            replaces the stored set whole, and leaving it out keeps it.

    AppendSheetSource:
      type: object
      properties:
        ref:
          type: string
          minLength: 1
          description: >-
            Spreadsheet id, optionally `id!Tab` to pin one tab. The sheet is
            pulled fresh on every append.
          example: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
        connectionId:
          type: string
          minLength: 1
          description: >-
            A Google connection, needed only for a sheet that is not publicly readable.
      required: [ref]

    AppendContactsResponse:
      type: object
      properties:
        results:
          type: array
          description: >-
            One entry per inline row, in the same order. Upload and sheet rows
            dropped at import appear only in `excluded`.
          items:
            $ref: '#/components/schemas/AppendResult'
        summary:
          type: object
          description: The `results` statuses counted up.
          properties:
            enrolled:
              type: integer
            rearmed:
              type: integer
            alreadyPending:
              type: integer
            inFlight:
              type: integer
            skipped:
              type: integer
        excluded:
          type: object
          description: >-
            Upload and sheet appends only: rows the import pipeline dropped before any write.
          properties:
            invalidPhone:
              type: integer
            missingName:
              type: integer
            duplicate:
              type: integer

    AppendResult:
      type: object
      properties:
        contactId:
          type: string
          description: >-
            `evt#{externalRef}` for a row that carried one, otherwise a generated uuid. Absent on rows skipped for a bad phone, a missing name, a bad `externalRef` or a duplicate phone.
          example: evt#ord-48812
        status:
          type: string
          enum: [enrolled, rearmed, already_pending, in_flight, skipped]
          description: >-
            `enrolled` is a contact that did not exist. `rearmed` is one that had
            finished — done, out of attempts, or skipped — and is now queued to be
            dialed again. `already_pending` and `in_flight` mean it is still
            waiting or on a call right now, so this row changed nothing.
        reason:
          type: string
          enum: [invalidPhone, missingName, duplicate, invalidExternalRef, campaignClosed]
          description: >-
            Only on `skipped`. `duplicate` is a collision inside this request, not
            with an existing contact. `campaignClosed` means the campaign was
            closed part-way through a large batch: earlier rows were written and
            the rest were not.
    CampaignCallList:
      type: object
      properties:
        calls:
          type: array
          items:
            $ref: '#/components/schemas/CampaignCallSummary'
        nextCursor:
          type: string
          description: >-
            Absent on the last page. Keep fetching while it is present — an intermediate page can come back empty without being the last.

    CampaignCallSummary:
      type: object
      description: >-
        A contact's enrollment joined to its most recent call. Everything from
        `outcome` down is read off that call's session record, so all of it is
        absent until the contact has been dialed.
      properties:
        callId:
          type: string
          description: >-
            Session id of the latest attempt.
            Pass it to the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes) for the transcript and recording.
            Absent while `attempts` is 0.
          example: session_N2MxZjlhYjQteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4Iys5MjMwMDEyMzQ1NjcjYTE
        contactId:
          type: string
          description: >-
            Identity within the campaign: the phone number as the contact list
            supplied it, or `evt#{externalRef}` for contacts appended under your
            own reference.
        name:
          type: string
        phone:
          type: string
          description: E.164, as dialed.
          example: '+923001234567'
        state:
          type: string
          description: >-
            What the dialer will do with this contact next — not how the last call
            went. `exhausted` means the retry budget ran out, `skipped` means the
            contact was excluded before dialing (do-not-call, a cap, a calling
            window, an unusable number).
          enum: [queued, dialing, retry_wait, done, exhausted, skipped]
        skippedReason:
          type: string
          enum: [window_closed, org_cap, campaign_cap, no_credits, dnc, invalid_number, recently_called]
          description: >-
            Only on `state: skipped`. The first four are about the moment the
            dialer looked, and can clear. `dnc`, `invalid_number` and
            `recently_called` are about the number itself.
        attempts:
          type: integer
          description: Dials placed so far. Never reset, so it also numbers the `callId`.
        cycle:
          type: integer
          description: >-
            The dial cycle the latest attempt is in. `1` for the first run. A callback redial or a recall starts the next one, with a fresh retry budget.
        cycleReason:
          type: string
          enum: [callback, recall]
          description: >-
            What started the current cycle. Absent on the first. `callback`: the contact asked to be called later. `recall`: a finished contact was queued again, by an append or a dial.
        nextAttemptAt:
          type: integer
          description: >-
            Epoch milliseconds at which this contact becomes dialable. Only
            meaningful while `state` is `queued` or `retry_wait`; terminal rows
            keep whatever it last held.
          example: 1787034062711
        outcome:
          type: string
          description: >-
            Why the last attempt did not become a conversation — the coarse bucket, with `failureReason` the finer one. Absent when the call connected. While `state` is `dialing` it reads `no_answer` as a placeholder, not a result.
          enum: [voicemail, busy, no_answer, unreachable, silent_pickup, failed]
        connected:
          type: boolean
          description: >-
            Whether the last attempt reached a live conversation. Whether it converted is a separate verdict, in [`/campaigns/{campaignId}/results`](/api-reference/campaign-outcomes/list-a-campaigns-outcomes).
        startedAt:
          type: string
          format: date-time
          description: When the last attempt was placed.
        durationSec:
          type: integer
          description: >-
            Talk time, answer to hangup. Absent on calls that were never answered.
        score:
          type: [integer, 'null']
          minimum: 0
          maximum: 100
          description: >-
            Percent of the applicable points earned. `null` when every rule was N/A.
            The campaign scorecard's grade, present once post-call analysis has run. The per-criterion breakdown is on the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes).
        failureReason:
          type: string
          description: >-
            The last attempt's failure, in the same vocabulary the session
            endpoints use. Absent when the call connected.
          enum:
            - wrong_number
            - busy
            - declined
            - no_answer
            - unreachable
            - voicemail
            - silent_pickup
            - network_error
            - call_failed
        failureReasonWithCode:
          type: string
          description: >-
            `failureReason` with the carrier's SIP code appended where there was one, as in `busy:486`.
          example: no_answer:408
        sentiment:
          type: string
          description: The grader's read on the caller. Present once graded.
          enum: [happy, neutral, annoyed]
        assistantVersion:
          type: integer
          description: >-
            The assistant version the latest attempt ran. Explains a change in
            behaviour mid-run.
        assistantAlias:
          type: string
          enum: [prod, draft]
          description: >-
            The alias the latest attempt was launched on. Absent when the
            campaign pins a version number.
        summary:
          allOf:
            - $ref: '#/components/schemas/ConversationSummary'
          description: >-
            Only with `include_summary=true`. Absent, not null, when there is
            none.
    CallTranscriptTurn:
      type: object
      properties:
        speaker:
          type: string
          enum: [agent, caller]
        text:
          type: string
        atSec:
          type: integer
          description: >-
            Seconds from the start of the recording, so it lines up with
            `audioUrl`.

    CallToolCall:
      type: object
      properties:
        name:
          type: string
        arguments:
          description: What the model passed, shaped by the tool's own parameter schema.
        result:
          type: string
          description: >-
            What the tool answered, JSON-encoded as a string. Absent if it never
            returned.
        callId:
          type: string
          description: Pairs the invocation with its result. Unrelated to this call's `callId`.
        atSec:
          type: integer

    ScorecardCriterion:
      type: object
      properties:
        ruleId:
          type: string
        title:
          type: string
          description: The rule as written in your rubric.
        points:
          type: integer
          description: What this rule is worth.
        earned:
          type: integer
          description: >-
            Scoring is all-or-nothing, so this is either `0` or the full
            `points`.
        passed:
          type: boolean

    CallConversion:
      type: object
      description: >-
        The outcome the assistant recorded. Its absence on a connected call means the goal was not met.
      properties:
        goalKind:
          type: string
          description: >-
            The campaign goal that produced this payload. Matches
            `payload.kind`, except `event`, whose payload kind is `rsvp`.
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
        payload:
          $ref: '#/components/schemas/ConversionPayload'
        slotIso:
          type: string
          format: date-time
          description: The time booked, on goals that book one.
        createdAt:
          type: string
          format: date-time

    CallbackRequest:
      description: >-
        Present when the contact asked to be called again. The campaign callbacks endpoint lists them across the whole campaign.
      allOf:
        - $ref: '#/components/schemas/SessionCallback'
    CallbackList:
      type: object
      properties:
        callbacks:
          type: array
          items:
            $ref: '#/components/schemas/Callback'
        nextCursor:
          type: string
          description: >-
            Pass back as `cursor` for the next page. Only its absence marks the last page; a short page does not.

    Callback:
      type: object
      description: One contact's request to be called again.
      properties:
        callbackId:
          type: string
          description: >-
            The callback's id.
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        assistantId:
          type: string
          format: uuid
          description: The assistant that was on the call.
        campaignId:
          type: string
          format: uuid
          description: Always the campaign in the path.
        sessionId:
          type: string
          description: >-
            The call that asked for it. Pass it to the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes) for the transcript.
        kind:
          $ref: '#/components/schemas/CallbackKind'
        reason:
          type: string
          description: One line on why.
        requestedText:
          type: string
          description: The contact's words that asked for the callback.
        callbackAfter:
          $ref: '#/components/schemas/CallbackAfter'
        timezone:
          type: string
          description: The IANA zone `callbackAfter` was resolved in.
          example: Asia/Karachi
        status:
          $ref: '#/components/schemas/CallbackStatus'
        redial:
          $ref: '#/components/schemas/CallbackRedial'
        timeline:
          type: array
          description: >-
            What happened to the callback, oldest first. Absent on older callbacks, whose history was not recorded.
          items:
            $ref: '#/components/schemas/CallbackEvent'
        createdAt:
          type: string
          format: date-time
          description: When the call was analysed, not when it was placed.
        updatedAt:
          type: string
          format: date-time
          description: Last change; equal to `createdAt` until one happens.

    CallbackKind:
      type: string
      enum: [human, later]
      description: >-
        `human`: a person must call back. `later`: call again, and the assistant may do it. On a campaign, a `later` callback requeues the contact for `callbackAfter`. Callbacks from before kinds existed have no `kind` and are all for a person.

    CallbackStatus:
      type: string
      enum: [open, scheduled, done, failed]
      description: >-
        `open`: a person must act. `scheduled`: the assistant calls at `callbackAfter`. `done`: a redial reached the contact, or a person closed it. `failed`: the redial cycle ended without reaching the contact. You can set `open` or `done` with [`PATCH /campaigns/callbacks/{callbackId}`](/api-reference/campaign-outcomes/update-a-callbacks-status).

    CallbackAfter:
      type: string
      format: date-time
      description: >-
        When to call, as ISO-8601 with the offset of `timezone`, like `2026-10-29T15:00:00+05:00`. Both kinds get the time the contact named. When no time was said, a `later` callback gets two hours after the call, and a `human` callback has none. Absent when the time is tied to an event or more than 30 days away. Older callbacks can hold free text here.

    CallbackRedial:
      type: object
      description: >-
        `later` callbacks on campaign calls only. What became of the assistant's redial.
      properties:
        status:
          type: string
          enum: [scheduled, disabled, limit_reached, time_unresolved, campaign_inactive, not_released, exhausted, skipped, campaign_closed]
          description: >-
            `scheduled`: the contact is requeued for `at`. `disabled`: the campaign has `dialing.redialCallbacks: false`. `limit_reached`: the contact already had 3 dial cycles. `time_unresolved`: no time could be held. `campaign_inactive`: the campaign had stopped. `not_released`: the contact could not be requeued. `exhausted`, `skipped`: the redial cycle ended without reaching the contact, and the callback is `failed`. `campaign_closed`: the campaign closed before the redial, and the callback is `open` again for a person.
        at:
          type: string
          format: date-time
          description: When the redial is due. Only with `scheduled`.

    CallbackEvent:
      type: object
      description: One entry in a callback's timeline.
      properties:
        type:
          type: string
          enum: [created, redial, reached, failed, returned, status, note]
          description: >-
            `created`: the call asked for it. `redial`: what became of the assistant's redial. `reached`: a redial reached the contact. `failed`: the redial cycle ran out. `returned`: the campaign closed first, so it is `open` again. `status`: someone set the status. `note`: someone wrote a note.
        at:
          type: string
          format: date-time
        by:
          type: string
          description: The identity that made a `status` change or `note`. Absent when the system did it.
        status:
          $ref: '#/components/schemas/CallbackStatus'
        redial:
          $ref: '#/components/schemas/CallbackRedial'
        text:
          type: string
          description: The note, on `note` events.
      required: [type, at]

    UpdateCallbackStatusRequest:
      type: object
      properties:
        status:
          type: string
          enum: [open, done]
          description: >-
            `done` marks the request handled; `open` reopens it.
      required: [status]

    UpdateCallbackStatusResponse:
      type: object
      properties:
        callbackId:
          type: string
          description: The callback's id.
        status:
          type: string
          enum: [open, done]

    AddCallbackNoteRequest:
      type: object
      properties:
        text:
          type: string
          description: The note. Not blank, at most 2,000 bytes in UTF-8 (about 1,000 Urdu characters).
      required: [text]
    CampaignStats:
      type: object
      description: >-
        Counters for one campaign. A dial lands in at most one of `connected`, `voicemail`, `noAnswer`, `silentPickup`, `failed` and `analysisFailed`, so those sum to `dialed` or just under. Grading, sentiment and revenue figures cover the `connected` subset.
      properties:
        dialed:
          type: integer
          description: Call attempts placed. A contact retried twice counts twice.
        connected:
          type: integer
          description: Attempts that reached a person and were graded.
        booked:
          type: integer
          description: >-
            Conversions recorded — appointments booked, leads qualified, payments promised, depending on the campaign's goal.
        notInterested:
          type: integer
        wrongPerson:
          type: integer
        voicemail:
          type: integer
        noAnswer:
          type: integer
        silentPickup:
          type: integer
          description: >-
            Answered, but the callee never spoke — a dead line, an unannounced
            machine, or a carrier answering on the callee's behalf. Not graded.
        failed:
          type: integer
          description: >-
            Attempts that never reached the callee (carrier rejection, no
            route, no credit). Busy and unreachable dials count in `dialed` but land in no bucket here.
        analysisFailed:
          type: integer
          description: >-
            Calls that reached a person but could not be scored. They count in no other counter here, not even `connected`.
        callbacks:
          type: integer
          description: Calls where the contact asked to be called again, of either kind. The campaign callbacks endpoint lists them.
        scored:
          type: integer
          description: >-
            Graded calls with a score, equal to `great` + `ok` + `low`. A call
            whose every rule was N/A has `score: null` and is left out.
        avgScore:
          type: [integer, 'null']
          description: Mean score over `scored`, rounded. `null` until a call has a score.
        great:
          type: integer
          description: Scored 80 or above.
        ok:
          type: integer
          description: Scored 60 to 79.
        low:
          type: integer
          description: Scored under 60.
        totalDurationSec:
          type: integer
        avgDurationSec:
          type: integer
          description: Rounded mean over the calls that reported a duration.
        totalContacts:
          type: integer
          description: Contacts enrolled, counted across every run.
        attemptedContacts:
          type: integer
          description: Contacts dialed at least once.
        resolvedContacts:
          type: integer
          description: Contacts the campaign is finished with — reached, out of retries, or skipped.
        retrying:
          type: integer
          description: Contacts waiting out a retry backoff before the next attempt.
        exhausted:
          type: integer
          description: Contacts that used every retry without reaching anyone. Counted in `resolvedContacts` too.
        projectedRevenue:
          type: integer
          description: >-
            `booked` priced with the campaign's `config.economics`, in the same unit those values used. 0 when economics were never configured.
        sentiment:
          type: object
          description: How post-call analysis read the contact's mood.
          properties:
            happy:
              type: integer
            neutral:
              type: integer
            annoyed:
              type: integer
            unknown:
              type: integer
              description: Connected calls the grader left unlabelled.
    ListConversionsResponse:
      type: object
      properties:
        conversions:
          type: array
          items:
            $ref: '#/components/schemas/ConversionRecord'
        nextCursor:
          type: string
          description: Absent on the last page.

    ConversionRecord:
      type: object
      description: The outcome one graded call produced.
      properties:
        sessionId:
          type: string
          description: >-
            The call this outcome came from.
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        assistantId:
          type: string
          format: uuid
        campaignId:
          type: string
          format: uuid
        ref:
          type: string
          description: Currently always equal to `sessionId`. The `ref` on the `conversion.created` webhook is the same string.
        goalKind:
          type: string
          description: The campaign's goal, copied onto every row.
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
        payload:
          $ref: '#/components/schemas/ConversionPayload'
        slotIso:
          type: string
          format: date-time
          description: >-
            The agreed appointment or event time, on goals that have one. Rows are ordered newest-first by this field, or by `createdAt` where it's absent.
        date:
          type: string
          format: date
          description: >-
            Day bucket for this outcome, in the campaign's calling-window timezone
            rather than UTC.
        createdAt:
          type: string
          format: date-time
          description: When post-call analysis graded the call, not when it was placed.

    ConversionPayload:
      description: >-
        What the agent got out of the call. `kind` tracks the campaign's
        `goalKind`, except that an `event` campaign records `rsvp` payloads. Every
        field but `kind` is best-effort — the agent omits whatever the caller
        never said.
      oneOf:
        - title: Appointment
          type: object
          properties:
            kind:
              type: string
              const: appointment
            outcome:
              type: string
            summary:
              type: string
        - title: RSVP
          type: object
          properties:
            kind:
              type: string
              const: rsvp
            outcome:
              type: string
              enum: [confirmed, maybe]
            party:
              type: integer
              minimum: 1
              description: Headcount the caller is bringing, including themselves.
            summary:
              type: string
        - title: Lead
          type: object
          properties:
            kind:
              type: string
              const: lead
            outcome:
              type: string
            status:
              type: string
              enum: [qualified, callback, not_interested, wrong_person, needs_human]
            interest:
              type: string
            nextStep:
              type: string
            notes:
              type: string
            summary:
              type: string
        - title: Order confirmation
          type: object
          properties:
            kind:
              type: string
              const: order_confirmation
            status:
              type: string
              enum: [confirmed, cancelled, amended, address_incomplete, wrong_person, callback]
            address:
              type: string
              description: >-
                Delivery address as the caller gave or corrected it.
            orderId:
              type: string
            orderValue:
              type: object
              properties:
                amount:
                  type: number
                currency:
                  type: string
            summary:
              type: string
        - title: Payment reminder
          type: object
          properties:
            kind:
              type: string
              const: payment_reminder
            status:
              type: string
              enum: [will_pay, already_paid, dispute, callback, wrong_person]
            promiseDate:
              type: string
              description: The date the caller committed to pay by.
            summary:
              type: string
        - title: Outreach
          type: object
          properties:
            kind:
              type: string
              const: outreach
            status:
              type: string
              enum: [interested, callback, not_interested, wrong_person]
            summary:
              type: string
        - title: Reservation confirmation
          type: object
          properties:
            kind:
              type: string
              const: reservation_confirmation
            status:
              type: string
              enum: [confirmed, cancelled, rescheduled, wrong_person, callback]
            party:
              type: integer
              minimum: 1
            reservationTime:
              type: string
            summary:
              type: string
    CampaignResults:
      type: object
      properties:
        artifactModel:
          description: The row shape this campaign's goal produces.
          type: string
          enum:
            - day_sheet
            - guest_list
            - lead_list
            - order_list
            - payment_list
            - outreach_list
            - reservation_list
        vocabulary:
          description: >-
            How this goal words a successful outcome, for labelling a UI: `booked` an `appointment`, `qualified` a `lead`.
          type: object
          properties:
            conversionVerb:
              type: string
            conversionNoun:
              type: string
        rows:
          description: >-
            Ordered newest first by `slotIso` where the goal has one, by
            `createdAt` otherwise.
          type: array
          items:
            $ref: '#/components/schemas/CampaignResultRow'
        nextCursor:
          description: Absent on the last page.
          type: string

    CampaignResultRow:
      type: object
      properties:
        sessionId:
          description: >-
            The call that produced this outcome. Pass it to the [session detail](/api-reference/sessions-%26-call-records/get-a-calls-transcript-and-outcomes) for the transcript and recording.
          type: string
        goalKind:
          type: string
          enum:
            - appointment
            - event
            - lead
            - order_confirmation
            - payment_reminder
            - outreach
            - reservation_confirmation
        name:
          description: >-
            Contact name as enrolled. Absent for contacts that carried no name.
          type: string
        phone:
          description: >-
            The number as supplied in the contact list — local (`0300…`) or E.164
            (`+92300…`), not normalized.
          type: string
        runId:
          description: The launch run that enrolled this contact.
          type: string
          format: uuid
        payload:
          $ref: '#/components/schemas/ConversionPayload'
        slotIso:
          description: >-
            The time the caller agreed to, on goals that carry one (appointment,
            event, reservation).
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
    PreviewContactSourceRequest:
      oneOf:
        - title: Google Sheet
          type: object
          required: [kind, ref]
          properties:
            kind:
              type: string
              enum: [google_sheets]
            ref:
              type: string
              description: >-
                The spreadsheet id, optionally followed by `!` and a tab or A1 range
                (`{id}!Contacts!A:E`). A bare id reads the first tab, columns A–ZZ.
            connectionId:
              type: string
              description: >-
                Reads through this Google connection instead of the public-link path.
            mapping:
              $ref: '#/components/schemas/ContactColumnMapping'
              description: Overrides column auto-detection.
        - title: Inline CSV or pasted rows
          type: object
          required: [kind, data]
          properties:
            kind:
              type: string
              enum: [csv_upload, paste]
              description: >-
                `csv_upload` for CSV text with a header row; `paste` for a grid or one-line-per-contact rows.
            data:
              type: string
              description: >-
                The rows themselves: CSV text for `csv_upload`; for `paste`, a tab/comma-separated grid or one `Name, +92…` per line.
            mapping:
              $ref: '#/components/schemas/ContactColumnMapping'
              description: Overrides column auto-detection.

    ContactSourcePreview:
      type: object
      properties:
        summary:
          $ref: '#/components/schemas/ImportSummary'
        detectedMapping:
          $ref: '#/components/schemas/ContactColumnMapping'
          description: >-
            The columns actually used, after applying `mapping`. When a name was joined from two columns, `nameColumn` reads `First Name + Last Name`.
        sample:
          type: array
          maxItems: 10
          description: The first ten contacts that would be dialed.
          items:
            $ref: '#/components/schemas/NormalizedContact'
        total:
          type: integer
          description: Rows read, before validation.
        columns:
          type: array
          items:
            type: string
          description: >-
            The header row as parsed — what a `mapping` override chooses from.
            Absent for free-form paste, which has no headers.
        mappingIssues:
          type: array
          items:
            type: string
            enum: [phoneColumnNotFound, nameColumnNotFound]
          description: >-
            Non-empty means column detection failed. An unmapped phone column then shows up as every row `invalidPhone` with an empty value.

    NormalizedContact:
      type: object
      properties:
        name:
          type: string
        phone:
          type: string
          description: >-
            Kept in the format the sheet used, PK national (`03…`) or E.164
            (`+92…`), with spaces and dashes stripped.
        attributes:
          type: object
          additionalProperties:
            type: string
          description: >-
            The remaining columns, handed to the assistant as a contact record at dial time. Truncated at 1500 bytes per contact.
    CreateContactSourceRequest:
      oneOf:
        - title: Public link-shared sheet
          type: object
          required: [name, kind, ref]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
              description: Label for the source.
            kind:
              type: string
              enum: [google_sheets]
            ref:
              type: string
              minLength: 1
              description: >-
                Spreadsheet id from the sheet URL, optionally `!` plus an A1 range or
                tab name (`{id}!Leads`, `{id}!A:C`). A bare id reads columns A–ZZ of
                the first tab. The sheet must be shared as anyone-with-the-link; it
                is read with the platform's own credential.
            mapping:
              $ref: '#/components/schemas/ContactColumnMapping'
        - title: Private sheet via Google connection
          type: object
          required: [name, kind, ref, connectionId]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
              description: Label for the source.
            kind:
              type: string
              enum: [google_sheets]
            ref:
              type: string
              minLength: 1
              description: >-
                Spreadsheet id from the sheet URL, optionally `!` plus an A1 range or
                tab name (`{id}!Leads`, `{id}!A:C`). A bare id reads columns A–ZZ of
                the first tab.
            connectionId:
              type: string
              format: uuid
              description: >-
                The Google Sheets connection you added on the [portal](https://upliftai.org/app/calling/webhooks), whose account can read the sheet. No link-sharing needed.
            mapping:
              $ref: '#/components/schemas/ContactColumnMapping'

    ContactSource:
      type: object
      properties:
        sourceId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
        kind:
          type: string
          enum: [google_sheets]
        ref:
          type: string
        connectionId:
          type: string
          format: uuid
        mapping:
          $ref: '#/components/schemas/ContactColumnMapping'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        version:
          type: integer
          description: Sources cannot be edited, so this stays `1`.
      required:
        [sourceId, organizationId, projectId, name, kind, ref, createdAt, updatedAt, version]
    Connection:
      type: object
      description: >-
        A project-level binding to an external system — where outcomes are
        delivered, which endpoint answers tool calls, or which account a tool
        books against.
      properties:
        connectionId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 120
          description: Label only — nothing routes on it.
        kind:
          type: string
          description: >-
            Fixed at creation — switching integrations means creating a new connection.
          enum:
            - webhook
            - tool_endpoint
            - google_calendar
            - sheets
        config:
          description: >-
            The kind's non-secret settings. Absent when the connection was
            created without any.
          oneOf:
            - title: Webhook or tool endpoint
              type: object
              properties:
                url:
                  type: string
                  description: Where deliveries or tool calls are POSTed.
            - title: Google Calendar
              type: object
              properties:
                calendarId:
                  type: string
                  description: >-
                    Where bookings land. Missing means the account's `primary`
                    calendar.
                accountEmail:
                  type: string
                  description: The connected Google account.
            - title: Google Sheets
              type: object
              properties:
                accountEmail:
                  type: string
                  description: The connected Google account.
        health:
          $ref: '#/components/schemas/ConnectionHealth'
        credentialsConfigured:
          type: boolean
          description: >-
            Whether any credential is stored. The credentials themselves are never returned.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          description: >-
            Last edit to `name`, `config` or credentials. Health probes don't move it.
        version:
          type: integer
          description: Starts at 1, incremented on every update.

    ConnectionHealth:
      type: object
      description: The outcome of the last probe.
      properties:
        state:
          type: string
          enum: [unknown, healthy, failed, failed_permanent]
          description: >-
            `unknown` until a probe runs, and permanently for `sheets`, which has no probe. `failed` is transient and worth re-probing; `failed_permanent` means the credential was rejected and someone has to re-authorize the connection.
        lastCheckedAt:
          type: string
          format: date-time
          description: Absent until the first probe.
        detail:
          type: string
          description: >-
            Free text from the probe, not a stable code — `ping 200`,
            `auth failed (401)`, `unexpected status 502`.
    ConnectionList:
      type: object
      properties:
        connections:
          type: array
          items:
            $ref: '#/components/schemas/Connection'
        nextCursor:
          type: string
          description: >-
            Pass as `cursor` for the next page. Its absence means you have them
            all.
    CreateConnectionRequest:
      description: >-
        `kind` is fixed at creation — the update endpoint cannot change it.
        Credentials are stored encrypted and never returned by any endpoint.
      oneOf:
        - title: Webhook
          type: object
          required: [name, kind, credentials]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
              description: Label only — nothing routes on it, and it need not be unique.
            kind:
              type: string
              enum: [webhook]
              description: Receives every call's events at `config.url`.
            credentials:
              type: object
              properties:
                signingSecret:
                  type: string
                  description: >-
                    The HMAC key deliveries are signed with, sent as `x-uplift-ai-signature`.
            config:
              type: object
              properties:
                url:
                  type: string
                  description: >-
                    Delivery endpoint. Must be `https` and must not point at a
                    loopback, private or link-local address. Not required — but a
                    webhook without a `url` is stored, then fails every delivery.
        - title: Tool endpoint
          type: object
          required: [name, kind, credentials]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
              description: Label only — nothing routes on it, and it need not be unique.
            kind:
              type: string
              enum: [tool_endpoint]
              description: >-
                Answers tool calls: the platform POSTs `{tool, args}` to
                `config.url` and the assistant speaks the response. Unlike
                `webhook`, it receives no call events — only the tools whose
                `execution.connectionId` points at it.
            credentials:
              type: object
              properties:
                signingSecret:
                  type: string
                  description: >-
                    The HMAC key tool calls are signed with, sent as `x-uplift-ai-signature`.
            config:
              type: object
              properties:
                url:
                  type: string
                  description: >-
                    Tool endpoint. Must be `https` and must not point at a
                    loopback, private or link-local address.
        - title: Google Calendar
          type: object
          required: [name, kind, credentials]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
            kind:
              type: string
              enum: [google_calendar]
              description: >-
                The account your assistant books appointments on — pick the
                target calendar from [its calendars](/voice-agents/api-reference/integration/list-a-google-connections-calendars).
                Created through our own frontend for now: Google's OAuth
                consent is needed to access the calendar.
            credentials:
              type: object
              properties:
                accessToken:
                  type: string
                refreshToken:
                  type: string
                expiresAt:
                  type: string
            config:
              type: object
              properties:
                calendarId:
                  type: string
                  description: >-
                    Where bookings land. Defaults to the literal `primary` — pick a
                    real id from [`/connections/{connectionId}/calendars`](/voice-agents/api-reference/integration/list-a-google-connections-calendars).
                accountEmail:
                  type: string
        - title: Google Sheets
          type: object
          required: [name, kind, credentials]
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
            kind:
              type: string
              enum: [sheets]
              description: >-
                The credential for reading private sheets — pass its
                `connectionId` when [registering a sheet source](/api-reference/google-sheet-sources/register-a-google-sheet-as-a-source)
                or [dry-running an import](/api-reference/contacts/dry-run-a-contact-import).
                No health probe; its state stays `unknown`. Created through our
                own frontend for now: Google's OAuth consent is needed to read
                a private sheet. A public link-shared sheet needs no connection
                at all — register it as a source directly.
            credentials:
              type: object
              properties:
                accessToken:
                  type: string
                refreshToken:
                  type: string
                expiresAt:
                  type: string
    UpdateConnectionRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: Label shown wherever the connection is picked. Required even when only `config` changes.
        credentials:
          description: >-
            Secret material — pick the shape matching the connection's `kind`.
            Omit it to keep what is stored; sending it replaces the stored set
            outright, and `{}` clears it.
          oneOf:
            - title: Webhook or tool endpoint
              type: object
              properties:
                signingSecret:
                  type: string
                  description: >-
                    The HMAC key deliveries and tool calls are signed with, sent as
                    `x-uplift-ai-signature`.
            - title: Google Calendar
              type: object
              description: >-
                Fresh credentials come from re-running Google's consent in our
                own frontend — rotate them there, not here.
              properties:
                accessToken:
                  type: string
                refreshToken:
                  type: string
                expiresAt:
                  type: string
            - title: Google Sheets
              type: object
              description: >-
                Fresh credentials come from re-running Google's consent in our
                own frontend — rotate them there, not here.
              properties:
                accessToken:
                  type: string
                refreshToken:
                  type: string
                expiresAt:
                  type: string
        config:
          description: >-
            The kind's non-secret settings — pick the shape matching the
            connection's `kind`. This replaces the stored object outright, and
            omitting it clears the stored config — resend the whole object on
            every call, even when only renaming.
          oneOf:
            - title: Webhook or tool endpoint
              type: object
              properties:
                url:
                  type: string
                  description: >-
                    Where deliveries or tool calls are POSTed. Must be `https` and must not
                    point at a loopback, private or link-local host.
            - title: Google Calendar
              type: object
              properties:
                calendarId:
                  type: string
                  description: Where bookings land.
                accountEmail:
                  type: string
                  description: The connected Google account.
            - title: Google Sheets
              type: object
              properties:
                accountEmail:
                  type: string
                  description: The connected Google account.
      required: [name]
    GoogleCalendarList:
      type: object
      properties:
        calendars:
          type: array
          items:
            $ref: '#/components/schemas/GoogleCalendarSummary'

    GoogleCalendarSummary:
      type: object
      properties:
        id:
          type: string
          description: >-
            Bind this as `calendarId`. The account's own calendar is listed under its email address; bookings with no `calendarId` set use the literal `primary` instead.
        summary:
          type: string
          description: Display name, falling back to `id` when Google returns none.
        primary:
          type: boolean
          description: True for the account's own calendar — at most one entry.
        accessRole:
          type: string
          description: >-
            What the connected account may do: `owner`, `writer`, `reader` or
            `freeBusyReader`. Only `owner` and `writer` can accept a booking. `unknown`
            when Google omits the field.
