Voygr Calls API

Programmatic outbound phone calls executed by an AI voice agent. Submit a task either as a plain-language brief or as a structured intent plus slots (inquiry, info_gathering, issue_resolution, booking, cancellation); the agent places the call, works through phone trees and hold, conducts the conversation in one of 13 languages, and returns a structured outcome with a full transcript. Calls are asynchronous — poll the call until it reaches a terminal state, or cursor-poll an SSE-formatted event feed for live progress and mid-call questions. A post-call merged transcript is rebuilt from the dual-channel recording. OpenAPI 3.1.0, 13 operations, X-API-Key auth, US destinations only, AI disclosure and recording notice on every call.

OpenAPI Specification

voygr-calls-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: Voygr Calls API
  description: "\nProgrammatic outbound phone calls executed by an AI voice agent. Submit a task,\nthe\
    \ agent places the call, conducts the conversation, and returns a structured\noutcome with a full\
    \ transcript.\n\n## Authentication\n\nEvery request needs the `X-API-Key` header. Keys are issued\
    \ per customer and\ncarry a credit quota, rate limits, and a concurrent-call limit.\n\n## Placing\
    \ a call\n\n`POST /calls` accepts two request shapes — pick one per call.\n\n**Freeform** — describe\
    \ the task in plain language:\n\n```bash\ncurl -s https://api.voygr.tech/calls   -H \"X-API-Key: $API_KEY\"\
    \ -H \"Content-Type: application/json\"   -d '{\"target_phone\": \"+15551234567\",\n       \"brief\"\
    : \"Ask what time the kitchen closes tonight.\",\n       \"language\": \"en\"}'\n```\n\n**Structured**\
    \ — a machine-readable `intent` plus `slots`, validated\ndeterministically before anything is dialed.\
    \ Five intents are supported:\n\n| Intent | Required slots |\n|---|---|\n| `inquiry` | `target_phone`,\
    \ `question` |\n| `info_gathering` | `target_phone`, `questions` |\n| `issue_resolution` | `target_phone`,\
    \ `issue_description` |\n| `booking` | `target_phone`, `name`, `date`, `time`, `party_size` |\n| `cancellation`\
    \ | `booking_id` — no phone; it comes from the stored booking |\n\nAn incomplete submission returns\
    \ `422` listing exactly what is missing, each\nslot with a `suggested_question` to relay to your user\
    \ — submit, collect,\nresubmit. Validation never dials and never charges. (The credit-hold check\n\
    runs first, so a key that cannot cover the hold gets `402` even for an\nincomplete submission.)\n\n\
    `language` accepts `en`, `es`, `fr`, `de`, `hi`, `ru`, `pt`, `ja`, `it`,\n`nl`, `sr`, `tr`, `pl`,\
    \ or `auto` (the default); any other code is refused\nwith `422 unsupported_language`.\n\nThe success\
    \ envelope differs by shape: freeform wraps the call object in\n`{\"call\": {...}}`; structured returns\
    \ a flat envelope with top-level\n`call_id`, `status_url`, and `answer_url`. Both are documented with\
    \ examples\non the operation below.\n\n## Call lifecycle\n\nCalls are asynchronous. Poll `GET /calls/{call_id}`\
    \ until `status` reaches\n`completed`, `failed`, or `cancelled` — typically 30-90 seconds. The terminal\n\
    response carries `outcome_type`, `outcome_summary`, and `transcript_full`.\nThere is no completion\
    \ webhook.\n\nFor live progress, poll `GET /calls/{call_id}/events?after_event_id=N` with\nshort requests,\
    \ advancing the cursor to the last `id:` you received — the\nbody is SSE-formatted text, but do NOT\
    \ hold a long-lived stream open, and use\nthe query cursor (the gateway strips the `Last-Event-ID`\
    \ header). If the\nagent needs input mid-call it emits an `ask_user` event — create the call\nwith\
    \ `ask_user_mode: \"stream\"` to receive these, and respond via\n`POST /calls/{call_id}/answer`. A\
    \ queued or ringing call can be aborted with\n`POST /calls/{call_id}/cancel`.\n\nOnce the call ends,\
    \ `GET /calls/{call_id}/transcript-merged` returns the\ncomplete two-sided transcript rebuilt from\
    \ the dual-channel recording —\nincluding speech the live pipeline never transcribed (IVR phone trees,\
    \ hold\nannouncements). The live `transcript_full` stays real-time-only; the merged\none is the complete\
    \ record. A `202 {\"status\": \"merger_pending\"}` means the\nmerge (which fires seconds after the\
    \ call ends) hasn't finished — retry\nafter the `transcript_ready` SSE event, or force it with\n`POST\
    \ /calls/{call_id}/transcript-merged/rebuild`.\n\n## Billing\n\nThe hold and the charge are different\
    \ numbers:\n\n| Event | Credits |\n|---|---|\n| Hold at dial time (`credits_reserved`) | 30 — frozen,\
    \ refundable, NOT a charge |\n| Successful outcome (`success_*`) — `credits_charge_on_success` | 10\
    \ |\n| Unsuccessful outcome (`failed_*` — no answer, voicemail, technical) | 0 |\n\nThe hold is released\
    \ at completion and you are only ever charged the settled\namount. Requests are rejected with `402`\
    \ when the available balance cannot\ncover the hold.\n\n`GET /v1/usage` reports `available` (spendable\
    \ right now) and\n`call_credit_hold` (what one call freezes), so `available // call_credit_hold`\n\
    is how many calls you can start.\n\n## Limits\n\n- Free keys: 5 requests/second, 10 requests/minute.\
    \ Paid keys: 10\n  requests/second, 100 requests/minute (`429` beyond).\n- Concurrent calls per key\
    \ are limited (`409` with `active_call_ids` at the\n  cap; typically 2).\n- Service maintenance windows\
    \ return `503` with a `resume_at` timestamp.\n\n## Acceptable use\n\nCalls are transactional and user-initiated\
    \ only: no telemarketing,\nsolicitation, bulk dialing, or harassment. The agent discloses that it\
    \ is an\nAI assistant and that the call is recorded; disclosure is not configurable.\nUnited States\
    \ destinations only. Recordings and transcripts are retained for\na maximum of 90 days.\n\n\nCredit\
    \ rates: validation, enrichment, business status and categorize are 1 credit per request. An AI booking\
    \ call is 10 credits. Credits never expire."
  version: 1.0.0
servers:
- url: https://api.voygr.tech
  description: Production (stated by the provider in the spec description curl examples, api.voygr.tech/docs,
    and callwright-skill SKILL.md)
paths:
  /health:
    get:
      tags:
      - core
      summary: Health check
      description: Check if the API server is running
      operationId: health_check_health_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
  /v1/usage:
    get:
      tags:
      - core
      summary: Get API usage statistics
      description: Get current usage, quota, and reset date for the API key.
      operationId: get_usage_v1_usage_get
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
        '401':
          description: Missing API key header
          content:
            application/json:
              example:
                success: false
                error: API key required. Include X-API-Key header.
                error_code: AUTHENTICATION_ERROR
                request_id: ad33009a-fdfe-4e25-9650-4a49099c3d4a
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Invalid, revoked, or unauthorized API key
          content:
            application/json:
              example:
                success: false
                error: Invalid or revoked API key
                error_code: AUTHENTICATION_ERROR
                request_id: 25b104f0-9ad0-4fc7-8530-c5d5ff8d9698
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /calls:
    post:
      tags:
      - calls
      summary: Place a call
      description: Place an outbound AI phone call. Two request shapes — freeform (target_phone + brief)
        or structured (target_phone + intent + slots). See the guide above for the 422 slot-retry loop.
      operationId: create_call
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallCreateRequest'
            examples:
              freeform:
                summary: Freeform brief
                value:
                  target_phone: '+15551234567'
                  brief: Call and ask what time the kitchen closes tonight. Thank them and hang up.
                  language: en
              structured:
                summary: Structured slots
                value:
                  target_phone: '+15551234567'
                  intent: inquiry
                  slots:
                    intent: inquiry
                    target_phone: '+15551234567'
                    question: What time does the kitchen close tonight?
      responses:
        '201':
          description: Call created. The envelope shape depends on which request path was used — freeform
            returns CallResponse (a `call` wrapper), structured returns the flat SkillRunResponse (top-level
            call_id, no wrapper). Either way, poll GET /calls/{call_id} afterward — that response is identical
            regardless of which path created the call.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/CallResponse'
                - $ref: '#/components/schemas/SkillRunResponse'
              examples:
                freeform_response:
                  summary: Freeform path -> CallResponse (call wrapper)
                  value:
                    call:
                      call_id: c_abc123
                      customer_id: cus_xyz
                      task_id: t_abc123
                      target_phone: '+15551234567'
                      language: en
                      status: dialing
                      call_sid: CAxxxxxxxx
                      started_at: null
                      ended_at: null
                      duration_sec: null
                      outcome_type: null
                      outcome_summary: null
                      outcome_charge_cents: 0
                      created_at: '2026-07-17T12:00:00Z'
                      has_recording: false
                      recording_url: null
                      reservation_signals: null
                      transcript_full: null
                      supervisor_decisions: null
                    task_id: t_abc123
                    credits_reserved: 10
                    credits_charge_on_success: 10
                    owner_pod: pod-3
                structured_response:
                  summary: Structured path -> SkillRunResponse (flat, no wrapper)
                  value:
                    skill_run_id: srun_c_abc123
                    call_id: c_abc123
                    call_sid: CAxxxxxxxx
                    owner_pod: pod-3
                    status: dialing
                    credits_reserved: 10
                    credits_charge_on_success: 10
                    status_url: /calls/c_abc123
                    answer_url: /calls/c_abc123/answer
                    recording_url: /calls/c_abc123/recording
                    replayed: false
                    expected_next_steps:
                    - '...'
        '202':
          description: 'Call queued (deployments that queue before dialing — the drainer hasn''t dialed
            yet). The envelope-by-path rule DIFFERS from 201: freeform returns QueuedCallResponse (a third,
            much smaller shape — call_id/queue_id/position/status only, no call_sid/owner_pod/credits_reserved
            at all), structured still returns SkillRunResponse but with status=''queued'', call_sid=null,
            owner_pod=null.'
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/QueuedCallResponse'
                - $ref: '#/components/schemas/SkillRunResponse'
              examples:
                freeform_queued:
                  summary: Freeform path, queued -> QueuedCallResponse
                  value:
                    call_id: c_abc123
                    queue_id: q_abc123
                    position: 0
                    status: queued
                structured_queued:
                  summary: 'Structured path, queued -> SkillRunResponse (credits_reserved=0: the gateway-mediated
                    NORMAL case for public-API callers — the gateway holds the reservation, not callwright)'
                  value:
                    skill_run_id: srun_c_abc123
                    call_id: c_abc123
                    call_sid: null
                    owner_pod: null
                    status: queued
                    credits_reserved: 0
                    status_url: /calls/c_abc123
                    answer_url: /calls/c_abc123/answer
                    recording_url: /calls/c_abc123/recording
                    replayed: false
                    expected_next_steps:
                    - '...'
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
        '402':
          description: Insufficient credits for the 30-credit hold.
          content:
            application/json:
              examples:
                gateway_precheck:
                  summary: Gateway pre-check (common path)
                  value:
                    detail:
                      error: insufficient credits
                callwright_race:
                  summary: 'Race: gateway''s quota view was stale'
                  value:
                    detail:
                      error: quota_exceeded
                      needed_credits: 200
                      checkout_url: /checkout/buy
        '403':
          description: Key not permitted to make this request.
          content:
            application/json:
              examples:
                tier_not_permitted:
                  value:
                    detail:
                      error: tier not permitted
                missing_entitlement:
                  value:
                    detail:
                      error: 'missing entitlement: booking'
                freeform_disabled:
                  summary: Freeform brief disabled for this key
                  value:
                    detail:
                      error_code: freeform_disabled
                      hint: Submit structured `slots` (see GET /skills) or ask ops to enable freeform
                        for this key.
        '409':
          description: Concurrent-call cap reached.
          content:
            application/json:
              examples:
                concurrent_call_not_allowed:
                  value:
                    detail:
                      error: concurrent_call_not_allowed
                      max_concurrent: 2
                      active_call_ids:
                      - c_abc123
        '422':
          description: Bad request body — four flavors, distinguished by error_code, plus standard Pydantic
            validation.
          content:
            application/json:
              examples:
                missing_brief:
                  summary: Freeform path, brief absent/empty
                  value:
                    detail:
                      error_code: missing_brief
                      hint: Provide `brief` (freeform) or `slots` (structured) — see GET /skills/concierge/manifest.
                missing_slots:
                  summary: Structured path, required slots absent (flagship retry loop)
                  value:
                    detail:
                      error_code: missing_slots
                      slot_schema_version: concierge-v1
                      intent_type: inquiry
                      missing_slots:
                      - slot_name: question
                        reason: the single question the agent must ask the venue
                        suggested_question: What should we ask them?
                        type_hint: free_text
                      invalid: {}
                      hint: Collect the listed slots from your user and resubmit with `slots` populated.
                unknown_intent:
                  summary: intent not a supported value
                  value:
                    detail:
                      error_code: unknown_intent
                      slot_schema_version: concierge-v1
                      supported_intents:
                      - booking
                      - cancellation
                      - info_gathering
                      - inquiry
                      - issue_resolution
                      hint: Pick a supported intent and resubmit.
                invalid_slots:
                  summary: A free-text slot exceeds 2000 characters
                  value:
                    detail:
                      error_code: invalid_slots
                      slot_schema_version: concierge-v1
                      invalid:
                        question: must be at most 2000 characters
                      hint: Shorten the listed free-text slots to at most 2000 characters and resubmit.
                pydantic_validation:
                  summary: Standard field validation, e.g. missing target_phone
                  value:
                    detail:
                    - type: missing
                      loc:
                      - body
                      - target_phone
                      msg: Field required
                      input:
                        intent: inquiry
                        slots:
                          intent: inquiry
        '429':
          description: 'Rate limited. Free keys: 5 req/s, 10 req/min. Paid keys: 10 req/s, 100 req/min.'
          content:
            application/json:
              examples:
                rate_limited:
                  value:
                    detail:
                      error: rate limit exceeded
        '503':
          description: Service in a maintenance window.
          content:
            application/json:
              examples:
                maintenance:
                  value:
                    detail:
                      error: maintenance
                      message: '...'
                      resume_at: '2026-07-17T13:00:00Z'
    get:
      tags:
      - calls
      summary: List calls
      description: List your own calls, most recent first. Each entry omits transcript_full/supervisor_decisions
        — fetch a single call for those.
      operationId: list_calls
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          default: 100
          minimum: 1
          maximum: 1000
          title: Limit
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CallDTO'
        '400':
          description: limit outside 1..1000.
          content:
            application/json:
              examples:
                bad_limit:
                  value:
                    detail: limit must be between 1 and 1000
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
  /calls/{call_id}:
    get:
      tags:
      - calls
      summary: Get a call
      description: Poll a single call. Returns the full detail fields (including transcript_full) that
        the list endpoint omits.
      operationId: get_call
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      - name: call_id
        in: path
        required: true
        schema:
          type: string
          title: Call Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CallDTO'
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
        '404':
          description: Call not found, or isn't yours.
          content:
            application/json:
              examples:
                not_found:
                  value:
                    detail: Call not found
  /calls/{call_id}/answer:
    post:
      tags:
      - calls
      summary: Answer a mid-call question
      description: Answer a question the agent asked mid-call. If no answer arrives before the wait window
        elapses, the call proceeds and wraps up gracefully rather than stalling indefinitely.
      operationId: answer_call
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      - name: call_id
        in: path
        required: true
        schema:
          type: string
          title: Call Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnswerRequest'
            examples:
              answer:
                value:
                  answer: Yes, hold the table until 7:30pm
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnswerResponse'
              examples:
                delivered:
                  value:
                    delivered: true
                    reason: delivered
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
  /calls/{call_id}/events:
    get:
      tags:
      - calls
      summary: Poll call events (SSE-formatted)
      description: 'Event log of a call in SSE text format (`id:` / `event:` / `data:` lines). POLL this
        endpoint with short requests and the `after_event_id` cursor — do NOT hold a long-lived stream
        open. The gateway strips the `Last-Event-ID` header; the query param is the only cursor. Event
        types: `status_change`, `ask_user` (answer promptly via POST /calls/{call_id}/answer; requires
        ask_user_mode=stream at call creation), `outcome` (terminal), `recording_ready`, `transcript_ready`
        (the post-call merged transcript is built — see GET /calls/{call_id}/transcript-merged).'
      operationId: get_call_events
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      - name: call_id
        in: path
        required: true
        schema:
          type: string
          title: Call Id
      - name: after_event_id
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          title: After Event Id
        description: Return only events with event_id greater than this cursor. Start at 0; advance to
          the last `id:` you received.
      responses:
        '200':
          description: SSE-formatted text; empty body when no new events.
          content:
            text/event-stream:
              schema:
                type: string
              examples:
                status_change:
                  value: 'id: 2077

                    event: status_change

                    data: {"event_id": 2077, "call_id": "c_abc123", "event_type": "status_change", "data":
                    {"status": "dialing"}}


                    '
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
        '404':
          description: Call not found, or isn't yours.
          content:
            application/json:
              examples:
                not_found:
                  value:
                    detail: Call not found
  /calls/{call_id}/transcript-merged:
    get:
      tags:
      - calls
      summary: Get the post-call merged transcript
      description: 'The post-call merged transcript: both sides of the call on one timeline, built from
        the dual-channel recording after the call ends. This is a different, fuller record than the live
        transcript_full rows, not a tidied-up copy of them — the recording captures the callee''s channel
        in full, so the merged transcript routinely contains speech the live pipeline never transcribed:
        IVR phone trees, hold and queue announcements, anything said before the STT gate opened. If a
        recording plainly contains something your transcript doesn''t, this endpoint is where to look.'
      operationId: get_transcript_merged
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      - name: call_id
        in: path
        required: true
        schema:
          type: string
          title: Call Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MergedTranscript'
              examples:
                merged:
                  value:
                    version: 1
                    duration_ms: 17240
                    calibration_delta_ms: 0
                    turns:
                    - speaker: hostess
                      start_ms: 300
                      end_ms: 6100
                      text: Thank you for calling. If you know your party's extension, you may dial it
                        at any time.
                      source: deepgram_batch_multichannel
                    - speaker: bot
                      start_ms: 6800
                      end_ms: 9200
                      text: Hi, this is an AI assistant calling to ask about your opening hours.
                      source: deepgram_batch_multichannel
        '202':
          description: 'The merger hasn''t run yet — it fires seconds after the call ends. Retry after
            the `transcript_ready` SSE event, poll this endpoint, or request the transcript directly with
            POST /calls/{call_id}/transcript-merged/rebuild. Note 202 is not a promise the transcript
            is coming: a merge that already ran and failed answers 202 too (the automatic trigger fires
            once per call and is not retried), so a 202 that persists well past the call''s end is the
            cue to force one rebuild rather than keep polling.'
          content:
            application/json:
              examples:
                merger_pending:
                  value:
                    status: merger_pending
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
        '404':
          description: Call not found or isn't yours — also returned for calls where the callee declined
            recording (no verbatim transcript may be served; the summary + outcome on GET /calls/{call_id}
            remain available).
          content:
            application/json:
              examples:
                not_found:
                  value:
                    detail: Transcript not available
  /calls/{call_id}/transcript-merged/rebuild:
    post:
      tags:
      - calls
      summary: Rebuild the merged transcript now
      description: Rebuild this call's merged transcript from the recording, and return it. The automatic
        merge trigger fires once per call and cannot be replayed, so a call whose merge failed at that
        moment — or one merged before a merger fix — would otherwise keep its stale or missing transcript
        forever. Use this to backfill past calls, or to retry after a 202 on the GET. Idempotent — it
        rebuilds from the recording and overwrites what was stored. Each request costs a full transcription
        pass over the recording, so don't poll with it — poll the GET above or wait for the `transcript_ready`
        SSE event.
      operationId: rebuild_transcript_merged
      parameters:
      - name: X-API-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: X-Api-Key
      - name: call_id
        in: path
        required: true
        schema:
          type: string
          title: Call Id
      responses:
        '200':
          description: The rebuilt transcript — same body as the GET.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MergedTranscript'
        '401':
          description: Missing or invalid X-API-Key.
          content:
            application/json:
              examples:
                missing_key:
                  summary: X-API-Key header absent
                  value:
                    detail:
                      error: API key required
                invalid_key:
                  summary: X-API-Key doesn't resolve to an active key
                  value:
                    detail:
                      error: invalid API key
        '404':
          description: Call not found or isn't yours — also returned for calls where the callee declined
            recording (no verbatim transcript may be served).
          content:
            application/json:
              examples:
                not_found:
                  value:
                    detail: Transcript not available
        '425':
          description: The recording isn't available yet —

# --- truncated at 32 KB (66 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/voygr/refs/heads/main/openapi/voygr-calls-api-openapi.yml