openapi: 3.1.0
info:
  title: VoiceCallingAI API
  version: "2026-06-04"
  description: AI voice calling — place calls, manage contacts and opt-outs, and receive signed webhooks with call outcomes.
servers:
  - url: https://api.voicecallingai.com/v1
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: "vca_..." }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties: { code: { type: string }, message: { type: string } }
    Page:
      type: object
      properties:
        data: { type: array, items: {} }
        has_more: { type: boolean }
    CallLight:
      type: object
      properties:
        id: { type: string, format: uuid }
        direction: { type: string, enum: [inbound, outbound] }
        from: { type: string }
        to: { type: string }
        status: { type: string }
        agent_id: { type: string, format: uuid }
        contact_id: { type: string, format: uuid, nullable: true }
        outcome: { type: string, nullable: true }
        talk_seconds: { type: integer, nullable: true }
        created_at: { type: string, format: date-time }
        external_ref: { type: string, nullable: true }
    CallFull:
      type: object
      description: "The full call object (also the `data` of full webhook payloads). Note: the identifier is `call_id` (not `id`); durations live under `timings`."
      properties:
        call_id: { type: string, format: uuid }
        direction: { type: string, enum: [inbound, outbound] }
        from: { type: string }
        to: { type: string }
        agent_id: { type: string, format: uuid }
        contact_id: { type: string, format: uuid, nullable: true }
        campaign_id: { type: string, format: uuid, nullable: true }
        status: { type: string }
        hangup_cause: { type: string, nullable: true }
        timings:
          type: object
          properties:
            queued_at: { type: string, format: date-time, nullable: true }
            dialed_at: { type: string, format: date-time, nullable: true }
            answered_at: { type: string, format: date-time, nullable: true }
            ended_at: { type: string, format: date-time, nullable: true }
            ring_seconds: { type: integer, nullable: true }
            talk_seconds: { type: integer, nullable: true }
            total_seconds: { type: integer, nullable: true }
        credits_charged: { type: number, nullable: true }
        outcome: { type: string, nullable: true }
        disposition: { type: string, nullable: true }
        summary: { type: object, nullable: true }
        external_ref: { type: string, nullable: true }
        recording_url: { type: string, description: "Present only when available (coming soon)" }
        transcript: { type: object, description: "Present only when available (coming soon)" }
    CallRequest:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [queued, dialing, completed, blocked, failed, canceled] }
        call_id: { type: string, format: uuid, nullable: true }
        to: { type: string }
        agent_id: { type: string, format: uuid }
        external_ref: { type: string, nullable: true }
        scheduled_for: { type: string, format: date-time }
        error_code: { type: string, nullable: true }
        error_detail: { type: string, nullable: true }
        dialed_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Contact:
      type: object
      properties:
        id: { type: string, format: uuid }
        phone_e164: { type: string }
        name: { type: string, nullable: true }
        email: { type: string, nullable: true }
        external_id: { type: string, nullable: true }
        variables: { type: object }
        metadata: { type: object }
        consent_status: { type: string, enum: [unknown, consented, withdrawn] }
        dnd_status: { type: string, enum: [unknown, clean, dnd] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    WebhookSubscription:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        url: { type: string }
        events: { type: array, items: { type: string } }
        status: { type: string, enum: [active, paused, failed] }
        signing_secret: { type: string, description: "Masked in lists; full value only at create/rotate." }
        consecutive_failures: { type: integer }
        last_success_at: { type: string, format: date-time, nullable: true }
        last_failure_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
    OptOutStatus:
      type: object
      properties:
        phone_e164: { type: string }
        dnd_status: { type: string, enum: [unknown, clean, dnd] }
        consent_status: { type: string, enum: [unknown, consented, withdrawn] }
    SubscriptionCreated:
      type: object
      description: "Returned by subscription create. The full signing_secret appears ONLY here (and on rotate)."
      properties:
        id: { type: string, format: uuid }
        signing_secret: { type: string }
        store_this: { type: string, description: "Reminder that the secret is shown once — store it now." }
  parameters:
    limit: { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
    startingAfter: { name: starting_after, in: query, schema: { type: string, format: uuid } }
  responses:
    Err401: { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    Err4xx: { description: Request error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
paths:
  /calls:
    get:
      summary: List calls
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/startingAfter'
        - { name: status, in: query, schema: { type: string } }
        - { name: direction, in: query, schema: { type: string, enum: [inbound, outbound] } }
        - { name: agent_id, in: query, schema: { type: string, format: uuid } }
        - { name: contact_id, in: query, schema: { type: string, format: uuid } }
        - { name: external_ref, in: query, schema: { type: string } }
        - { name: created_after, in: query, schema: { type: string, format: date-time } }
        - { name: created_before, in: query, schema: { type: string, format: date-time } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page' } } } }
        "401": { $ref: '#/components/responses/Err401' }
    post:
      summary: Create a call (queues a call request)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_id, to]
              properties:
                agent_id: { type: string, format: uuid }
                to: { type: string, description: E.164 }
                contact_id: { type: string, format: uuid }
                variables: { type: object }
                external_ref: { type: string }
                idempotency_key: { type: string }
                scheduled_for: { type: string, format: date-time }
                schedule_if_outside_hours: { type: boolean, default: false }
      responses:
        "202": { description: Queued, content: { application/json: { schema: { $ref: '#/components/schemas/CallRequest' } } } }
        "200": { description: Idempotent replay, content: { application/json: { schema: { $ref: '#/components/schemas/CallRequest' } } } }
        "402": { $ref: '#/components/responses/Err4xx' }
        "422": { $ref: '#/components/responses/Err4xx' }
  /calls/{id}:
    get:
      summary: Get a call
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/CallFull' } } } }
        "404": { $ref: '#/components/responses/Err4xx' }
  /calls/{id}/transcript:
    get:
      summary: Get a call transcript (coming soon)
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses: { "404": { $ref: '#/components/responses/Err4xx' } }
  /calls/{id}/recording:
    get:
      summary: Get a signed recording URL (coming soon)
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses: { "404": { $ref: '#/components/responses/Err4xx' } }
  /call-requests:
    get:
      summary: List call requests
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/startingAfter'
        - { name: status, in: query, schema: { type: string } }
        - { name: external_ref, in: query, schema: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page' } } } }
  /call-requests/{id}:
    get:
      summary: Get a call request
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/CallRequest' } } } }
        "404": { $ref: '#/components/responses/Err4xx' }
  /call-requests/{id}/cancel:
    post:
      summary: Cancel a queued call request
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: Canceled, content: { application/json: { schema: { $ref: '#/components/schemas/CallRequest' } } } }
        "409": { $ref: '#/components/responses/Err4xx' }
  /contacts:
    get:
      summary: List contacts
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/startingAfter'
        - { name: phone, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string } }
        - { name: dnd_status, in: query, schema: { type: string, enum: [unknown, clean, dnd] } }
        - { name: consent_status, in: query, schema: { type: string, enum: [unknown, consented, withdrawn] } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page' } } } }
    post:
      summary: Create a contact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_e164]
              properties:
                phone_e164: { type: string }
                name: { type: string }
                metadata: { type: object }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
        "409": { $ref: '#/components/responses/Err4xx' }
  /contacts/{id}:
    get:
      summary: Get a contact
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
        "404": { $ref: '#/components/responses/Err4xx' }
    patch:
      summary: Update a contact
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { name: { type: string }, metadata: { type: object } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
  /opt-outs:
    post:
      summary: Opt a number out (sets DND)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_e164]
              properties: { phone_e164: { type: string }, reason: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/OptOutStatus' } } } }
  /opt-outs/{phone_e164}:
    get:
      summary: Check a number's opt-out state
      parameters: [ { name: phone_e164, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/OptOutStatus' } } } }
        "404": { $ref: '#/components/responses/Err4xx' }
    delete:
      summary: Clear an opt-out
      parameters: [ { name: phone_e164, in: path, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/OptOutStatus' } } } }
  /wallet:
    get:
      summary: Get wallet balance
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance: { type: number }
                  currency: { type: string, example: INR }
                  last_transaction_at: { type: string, format: date-time, nullable: true }
  /webhook-subscriptions:
    get:
      summary: List webhook subscriptions
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page' } } } }
    post:
      summary: Create a webhook subscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, events]
              properties:
                name: { type: string }
                url: { type: string, description: Must be https }
                events: { type: array, items: { type: string, enum: [call.ringing, call.answered, call.completed, call.failed, call.no_answer, call.busy, webhook.test] } }
                description: { type: string }
      responses:
        "201": { description: "Created — {id, signing_secret, store_this}; the secret is shown once", content: { application/json: { schema: { $ref: '#/components/schemas/SubscriptionCreated' } } } }
        "409": { $ref: '#/components/responses/Err4xx' }
        "422": { $ref: '#/components/responses/Err4xx' }
  /webhook-subscriptions/{id}:
    patch:
      summary: Update a subscription (url, events, status active|paused)
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookSubscription' } } } }
    delete:
      summary: Delete a subscription and its delivery history
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: Deleted, content: { application/json: { schema: { type: object, properties: { deleted: { type: boolean }, id: { type: string, format: uuid } } } } } }
  /webhook-subscriptions/{id}/rotate-secret:
    post:
      summary: Rotate the signing secret (shown once)
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: "New secret (shown once)", content: { application/json: { schema: { type: object, properties: { signing_secret: { type: string }, store_this: { type: string } } } } } }
  /webhook-subscriptions/{id}/test:
    post:
      summary: Send a webhook.test event to this subscription
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "202": { description: Queued, content: { application/json: { schema: { type: object, properties: { queued: { type: boolean }, delivery_id: { type: string, format: uuid }, event_id: { type: string, format: uuid } } } } } }
        "409": { $ref: '#/components/responses/Err4xx' }
