---
title: "Place a call"
method: POST
path: "/calls"
tags: ["calls"]
---

# Place a call

`POST /calls`

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.

## Headers

- `X-API-Key` string, nullable

## Request body

- union — Two shapes - pick one per call. Freeform: target_phone + brief (+ optional language). Structured: intent + slots (one of inquiry, info_gathering, issue_resolution, booking, cancellation). target_phone is required on the freeform path only - the structured path reads slots.target_phone, and cancellation needs no phone at all.
  - object
    - `target_phone` string, required — E.164 format.
    - `brief` string, nullable, required — Freeform natural-language task; becomes the agent's system prompt. Required on the freeform path.
    - `language` 'en' | 'es' | 'fr' | 'de' | 'hi' | 'ru' | 'pt' | 'ja' | 'it' | 'nl' | 'sr' | 'tr' | 'pl' | 'auto' — ISO 639-1 code or `auto`. Unsupported codes are refused with 422 unsupported_language.
    - `intent` 'inquiry' | 'info_gathering' | 'issue_resolution' | 'booking' | 'cancellation', nullable — Structured path only.
    - `slots` object, nullable — Structured path only. Required slots per intent: inquiry -> target_phone, question; info_gathering -> target_phone, questions; issue_resolution -> target_phone, issue_description; booking -> target_phone, name, date, time, party_size; cancellation -> booking_id.
    - `kind` 'restaurant_booking' | 'restaurant_cancel' | 'doctor_appointment' | 'hotel_booking' | 'concierge' | 'other' — Optional freeform-path task category. Purely a routing/analytics hint - the brief still carries the actual task. Ignored on the structured path (intent decides).
    - `ask_user_mode` 'any' | 'stream' — Routing for the agent's mid-call ask_user questions. `stream` delivers them as `ask_user` events on GET /calls/{call_id}/events (answer via POST /calls/{call_id}/answer) - the right choice for API clients. `any` (default) tries the legacy operator channels first; API-only integrations may never see the question.
  - object
    - `target_phone` string — E.164 format.
    - `brief` string, nullable — Freeform natural-language task; becomes the agent's system prompt. Required on the freeform path.
    - `language` 'en' | 'es' | 'fr' | 'de' | 'hi' | 'ru' | 'pt' | 'ja' | 'it' | 'nl' | 'sr' | 'tr' | 'pl' | 'auto' — ISO 639-1 code or `auto`. Unsupported codes are refused with 422 unsupported_language.
    - `intent` 'inquiry' | 'info_gathering' | 'issue_resolution' | 'booking' | 'cancellation', nullable — Structured path only.
    - `slots` object, required
    - `kind` 'restaurant_booking' | 'restaurant_cancel' | 'doctor_appointment' | 'hotel_booking' | 'concierge' | 'other' — Optional freeform-path task category. Purely a routing/analytics hint - the brief still carries the actual task. Ignored on the structured path (intent decides).
    - `ask_user_mode` 'any' | 'stream' — Routing for the agent's mid-call ask_user questions. `stream` delivers them as `ask_user` events on GET /calls/{call_id}/events (answer via POST /calls/{call_id}/answer) - the right choice for API clients. `any` (default) tries the legacy operator channels first; API-only integrations may never see the question.

## Response `201`

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.

- union
  - CallResponse — POST /calls success response (201, or 202 on deployments that queue before dialing).
    - `call` CallDTO, required — A call. GET /calls list entries omit transcript_full/supervisor_decisions - fetch GET /calls/{call_id} for those.
      - `call_id` string, required
      - `customer_id` string, required
      - `task_id` string, required
      - `target_phone` string, required
      - `language` 'en' | 'es' | 'fr' | 'de' | 'hi' | 'ru' | 'pt' | 'ja' | 'it' | 'nl' | 'sr' | 'tr' | 'pl' | 'auto', required
      - `status` 'queued' | 'dialing' | 'in_progress' | 'completed' | 'failed' | 'cancelled', required
      - `call_sid` string, nullable — The Twilio Call SID. `null` until dialing starts.
      - `started_at` string, date-time, nullable
      - `ended_at` string, date-time, nullable
      - `duration_sec` integer, nullable
      - `outcome_type` 'success_booked' | 'success_refused' | 'success_no_booking' | 'failed_no_answer' | 'failed_voicemail' | 'failed_busy' | 'failed_short_hangup' | 'failed_technical' | 'failed_no_agent_available', nullable — Set once the call is terminal. success_* outcomes are billed 10 credits; failed_* are billed 0. failed_no_agent_available = the venue kept the agent in a hold queue past the hold budget and no human ever picked up.
      - `outcome_summary` string, nullable
      - `outcome_charge_cents` integer — Despite the name, this is in the same credit unit as credits_reserved. A successful call settles at 10 (the charge), unsuccessful at 0 - not the 30 held.
      - `created_at` string, date-time, required
      - `has_recording` boolean
      - `recording_url` string, nullable — A RELATIVE path to this service's recording proxy endpoint (/calls/{call_id}/recording) when a recording exists - never the raw Twilio mp3 URL. `null` when has_recording is false. Retrieval isn't documented here.
      - `reservation_signals` string[], nullable — Short free-text signals (<10 short strings) about the reservation outcome. Always included (not detail-gated like transcript_full).
      - `transcript_full` TranscriptTurn[], nullable — Only populated on GET /calls/{call_id}, not on the GET /calls list.
        - `ts` string, date-time, required
        - `role` 'operator' | 'bot' | 'supervisor_stt' | 'system', required — operator = the callee, bot = the agent, supervisor_stt = a parallel transcription of the operator's side, system = lifecycle markers.
        - `text` string, required
      - `supervisor_decisions` object[], nullable — Only populated on GET /calls/{call_id}, not on the GET /calls list (same heavy-field gating as transcript_full).
    - `task_id` string, required
    - `credits_reserved` integer, required — The refundable HOLD placed at dial time (10). Not a charge - released at completion, minus credits_charge_on_success if the outcome is billable.
    - `credits_charge_on_success` integer — What the call actually costs if it reaches a billable outcome (10). Unsuccessful outcomes cost 0 and the whole hold comes back.
    - `owner_pod` string, required
  - SkillRunResponse — POST /calls success response for the STRUCTURED (slots) path ONLY. The structured branch runs through the same pipeline as the skills engine and returns this flat envelope instead of CallResponse - no `call` wrapper, top-level call_id. 201 Created (dial started immediately), or 202 Accepted with status='queued', owner_pod=null and call_sid=null (drainer hasn't dialed yet) on deployments that queue before dialing. Whichever path created the call, poll GET /calls/{call_id} (the status_url) the same way afterward - the call object itself is identical from there on.
    - `skill_run_id` string, required
    - `call_id` string, required
    - `call_sid` string, nullable, required — null until the drainer dials (e.g. status='queued').
    - `owner_pod` string, nullable, required — null until the drainer dials (e.g. status='queued').
    - `status` 'queued' | 'dialing' | 'in_progress' | 'completed' | 'failed' | 'cancelled', required
    - `credits_reserved` integer, required — The refundable HOLD, not a charge: 10 on the synchronous (immediate-dial) path. On the queued (202) path this is 0 for a GATEWAY-MEDIATED request - the NORMAL case for public-API callers - because the gateway already holds the reservation; callwright reports what it itself holds locally (0) so a reconciler never over-refunds. See credits_charge_on_success for what the run actually costs.
    - `credits_charge_on_success` integer — What the run costs if the call reaches a billable outcome (10). Unsuccessful outcomes cost 0.
    - `status_url` string, required — Same as GET /calls/{call_id}.
    - `answer_url` string, required
    - `recording_url` string, required — Always a relative path to this service's recording proxy (/calls/{call_id}/recording) - never null, even before a recording exists (unlike CallDTO.recording_url).
    - `replayed` boolean
    - `expected_next_steps` string[], required

## Other responses

- `202` — 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.
- `401` — Missing or invalid X-API-Key.
- `402` — Insufficient credits for the 30-credit hold.
- `403` — Key not permitted to make this request.
- `409` — Concurrent-call cap reached.
- `422` — Bad request body - four flavors, distinguished by error_code, plus standard Pydantic validation.
- `429` — Rate limited. Free keys: 5 req/s, 10 req/min. Paid keys: 10 req/s, 100 req/min.
- `503` — Service in a maintenance window.

---

[API](https://skmtc.dev/voygr/apis/voygr-calls-api.md) · [All operations](https://skmtc.dev/voygr/apis/voygr-calls-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/voygr/voygr-calls-api/revisions/a505c0ec5698/schema)
