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.

Operations 11

POST /calls Place a call #
GET /calls List calls #
GET /calls/{call_id} Get a call #
POST /calls/{call_id}/answer Answer a mid-call question #
GET /calls/{call_id}/events Poll call events (SSE-formatted) #
GET /calls/{call_id}/transcript-merged Get the post-call merged transcript #
POST /calls/{call_id}/transcript-merged/rebuild Rebuild the merged transcript now #
POST /calls/{call_id}/cancel Cancel a queued or active call #
GET /skills List available skills #
GET /skills/{skill_id}/manifest Get a skill's manifest (slot schemas) #
GET /users/me Who am I / quota snapshot #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/voygr-calls-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no email required.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

voygr-calls-api-openapi.yml Raw ↑
openapi: 3.2.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\nruns 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\nresponse 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`\nis 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)
tags:
- name: calls
paths:
  /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 — the call may still be finalizing. Nothing failed; retry shortly. On an old call, though, a persistent 425 means the recording no longer exists (retention purge) and the transcript cannot be rebuilt.
          content:
            application/json:
              examples:
                recording_pending:
                  value:
                    status: recording_pending
        '503':
          description: The rebuild hit a dependency failure. Safe to retry.
          content:
            application/json:
              examples:
                merge_failed:
                  value:
                    status: merge_failed
        '504':
          description: The gateway gave up waiting for the rebuild. The rebuild usually still completes and persists on the backend — recover the result with the free GET /calls/{call_id}/transcript-merged instead of re-POSTing.
          content:
            application/json:
              examples:
                backend_timeout:
                  value:
                    error: backend timeout
                    error_code: BACKEND_TIMEOUT
  /calls/{call_id}/cancel:
    post:
      tags:
      - calls
      summary: Cancel a queued or active call
      description: 'Best-effort cancel. Returns `{"cancelled": true}` when the call was still cancellable (queued/dialing), `{"cancelled": false}` when it already reached a terminal state — safe to call idempotently.'
      operationId: cancel_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:
                type: object
                properties:
                  cancelled:
                    type: boolean
                    title: Cancelled
                required:
                - cancelled
              examples:
                already_terminal:
                  value:
                    cancelled: false
        '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:
     

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