Skip to content

Place a call

POST /calls

https://api.voygr.tech/calls

Open in playground

Overview

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.

Authentication

  • API key in header X-API-Key: Your customer API key (pk_live_...), sent on every request. Self-serve at https://api.voygr.tech/checkout.

Request body

required

application/json

Schema: CallCreateRequest

Freeform brief

{
  "target_phone": "+15551234567",
  "brief": "Call and ask what time the kitchen closes tonight. Thank them and hang up.",
  "language": "en"
}

Structured slots

{
  "target_phone": "+15551234567",
  "intent": "inquiry",
  "slots": {
    "intent": "inquiry",
    "target_phone": "+15551234567",
    "question": "What time does the kitchen close tonight?"
  }
}

Responses

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.

application/json

Schema: CallResponse | SkillRunResponse

Freeform path - CallResponse (call wrapper)

{
  "call": {
    "call_id": "c_abc123",
    "customer_id": "cus_xyz",
    "target_phone": "+15551234567",
    "language": "en",
    "status": "dialing",
    "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
  },
  "credits_reserved": 0,
  "credits_charge_on_success": 10
}

Structured path - SkillRunResponse (flat, no wrapper)

{
  "call_id": "c_abc123",
  "status": "dialing",
  "credits_reserved": 0,
  "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

Call accepted for later dialing. Freeform returns QueuedCallResponse; structured returns SkillRunResponse with status=queued. Poll GET /calls/{call_id} for progress.

application/json

Schema: QueuedCallResponse | SkillRunResponse

Freeform path, queued - QueuedCallResponse

{
  "call_id": "c_abc123",
  "position": 0,
  "status": "queued"
}

Structured queued response; creditsreserved=0 does not mean no balance is held

{
  "call_id": "c_abc123",
  "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

Missing or invalid X-API-Key.

application/json

X-API-Key header absent

{
  "detail": {
    "error": "API key required"
  }
}

X-API-Key doesn't resolve to an active key

{
  "detail": {
    "error": "invalid API key"
  }
}

402

Insufficient credits for the 30-credit refundable hold.

application/json

Insufficient available credits

{
  "detail": {
    "error": "quota_exceeded",
    "needed_credits": 30,
    "checkout_url": "/checkout/buy"
  }
}

403

Key not permitted to make this request.

application/json

tiernotpermitted

{
  "detail": {
    "error": "tier not permitted"
  }
}

missingentitlement

{
  "detail": {
    "error": "missing entitlement: booking"
  }
}

Freeform brief disabled for this key

{
  "detail": {
    "error_code": "freeform_disabled",
    "hint": "Submit structured `slots` (see GET /skills) or ask ops to enable freeform for this key."
  }
}

409

Concurrent-call cap reached.

application/json

concurrentcallnotallowed

{
  "detail": {
    "error": "concurrent_call_not_allowed",
    "max_concurrent": 2,
    "active_call_ids": [
      "c_abc123"
    ]
  }
}

422

Bad request body - four flavors, distinguished by error_code, plus standard Pydantic validation.

application/json

Freeform path, brief absent/empty

{
  "detail": {
    "error_code": "missing_brief",
    "hint": "Provide `brief` (freeform) or `slots` (structured) - see GET /skills/concierge/manifest."
  }
}

Structured path, required slots absent (flagship retry loop)

{
  "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."
  }
}

intent not a supported 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."
  }
}

A free-text slot exceeds 2000 characters

{
  "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."
  }
}

Standard field validation, e.g. missing targetphone

{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "target_phone"
      ],
      "msg": "Field required",
      "input": {
        "intent": "inquiry",
        "slots": {
          "intent": "inquiry"
        }
      }
    }
  ]
}

429

Limits depend on your key; handle 429 responses and retry guidance.

application/json

ratelimited

{
  "detail": {
    "error": "rate limit exceeded"
  }
}

503

Service in a maintenance window.

application/json

maintenance

{
  "detail": {
    "error": "maintenance",
    "message": "...",
    "resume_at": "2026-07-17T13:00:00Z"
  }
}

Schemas

CallCreateRequest

Type: any | object

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.

FieldTypeRequiredDescription and constraints
target_phonestringoptionalE.164 format.
Example: "+15551234567"
briefstring | nulloptionalPlain-language task for the call. Required for freeform requests.
languagestringoptionalISO 639-1 code or auto. Unsupported codes are refused with 422 unsupported_language.
one of en, es, fr, de, hi, ru, pt, ja, it, nl, sr, tr, pl, auto; default auto
intentstring (one of inquiry, info_gathering, issue_resolution, booking, cancellation) | nulloptionalStructured path only.
slotsobject | nulloptionalStructured 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.
kindstringoptionalOptional freeform task category; brief defines the task. Ignored for structured requests, which use intent.
one of restaurant_booking, restaurant_cancel, doctor_appointment, hotel_booking, concierge, other; default other
ask_user_modestringoptionalUse stream to receive ask_user events through GET /calls/{call_id}/events and answer via POST /calls/{call_id}/answer. With any (default), an API-only client may not receive the question.
one of any, stream; default any

One of

  • Freeform: requires target_phone, brief
  • Structured: requires slots

CallResponse

Type: object

Freeform call success envelope. A 201 means dialing has started; accepted calls can return 202. Poll GET /calls/{call_id} for progress.

FieldTypeRequiredDescription and constraints
callCallDTOrequired
credits_reservedintegerrequiredReported call-local reservation, not the final charge or available balance. It is 0 for calls through api.voygr.tech, including immediately started and queued calls; a separate refundable balance hold still applies. Read GET /v1/usage for available credits and call_credit_hold.
Example: 0
credits_charge_on_successintegeroptionalSuccessful-outcome charge: 10 credits. Unsuccessful outcomes cost 0 and the hold is released.
Example: 10

SkillRunResponse

Type: object

Structured call success envelope with top-level call_id and no call wrapper. A 201 means dialing started; a 202 has status=queued. Poll status_url (GET /calls/{call_id}) for progress.

FieldTypeRequiredDescription and constraints
call_idstringrequiredExample: "c_abc123"
statusstringrequiredone of queued, dialing, in_progress, completed, failed, cancelled
credits_reservedintegerrequiredReported call-local reservation, not the final charge or available balance. It is 0 for calls through api.voygr.tech, including immediately started and queued calls; a separate refundable balance hold still applies. Read GET /v1/usage for available credits and call_credit_hold.
Example: 0
credits_charge_on_successintegeroptionalSuccessful-outcome charge: 10 credits. Unsuccessful outcomes cost 0.
Example: 10
status_urlstringrequiredSame as GET /calls/{call_id}.
Example: "/calls/c_abc123"
answer_urlstringrequiredExample: "/calls/c_abc123/answer"
recording_urlstringrequiredRelative path /calls/{call_id}/recording. Present even before a recording exists, unlike CallDTO.recording_url.
Example: "/calls/c_abc123/recording"
replayedbooleanoptionaldefault False
expected_next_stepsarray of stringrequired

QueuedCallResponse

Type: object

Freeform call accepted with 202 for later dialing. Poll GET /calls/{call_id} until status leaves queued. Structured requests use SkillRunResponse instead.

FieldTypeRequiredDescription and constraints
call_idstringrequiredExample: "c_abc123"
positionintegerrequiredQueue position; 0 is the front. Another call may still be starting when this is 0.
Example: 0
statusstringrequiredone of queued; default queued
credits_reservedintegeroptionalReported call-local reservation, not the final charge or available balance. It is 0 for calls through api.voygr.tech, including immediately started and queued calls; a separate refundable balance hold still applies. Read GET /v1/usage for available credits and call_credit_hold.
Example: 0
credits_charge_on_successintegeroptionalSuccessful-outcome charge: 10 credits. Unsuccessful outcomes cost 0.
Example: 10

CallDTO

Type: object

A call. GET /calls list entries omit transcript_full; fetch GET /calls/{call_id} for the transcript.

FieldTypeRequiredDescription and constraints
call_idstringrequiredExample: "c_abc123"
customer_idstringrequiredExample: "cus_xyz"
target_phonestringrequiredExample: "+15551234567"
languagestringrequiredone of en, es, fr, de, hi, ru, pt, ja, it, nl, sr, tr, pl, auto
statusstringrequiredone of queued, dialing, in_progress, completed, failed, cancelled
started_atstring (date-time) | nulloptional
ended_atstring (date-time) | nulloptional
duration_secinteger | nulloptional
outcome_typestring (one of success_booked, success_refused, success_no_booking, failed_no_answer, failed_voicemail, failed_busy, failed_short_hangup, failed_technical, failed_no_agent_available, failed_no_disclosure, failed_call_dropped, failed_wrong_number, failed_cancelled, failed_no_engagement, failed_agent_mute) | nulloptionalSet when the call is terminal. success_* outcomes cost 10 credits; failed_* cost 0. failed_no_agent_available means no human answered before the call's hold-time limit.
outcome_summarystring | nulloptional
outcome_charge_centsintegeroptionalSettled charge in credits, despite the field name: 10 for a successful call, 0 for an unsuccessful call. This is not the refundable hold.
default 0
created_atstring (date-time)required
has_recordingbooleanoptionaldefault False
recording_urlstring | nulloptionalRelative recording path /calls/{call_id}/recording when a recording exists; null when has_recording is false. Recording retrieval is not documented here.
reservation_signalsarray of string | nulloptionalShort free-text signals (<10 short strings) about the reservation outcome. Always included (not detail-gated like transcript_full).
transcript_fullarray of TranscriptTurn | nulloptionalOnly populated on GET /calls/{call_id}, not on the GET /calls list.

TranscriptTurn

Type: object

One line of a call transcript.

FieldTypeRequiredDescription and constraints
tsstring (date-time)required
rolestringrequiredoperator = the callee; bot = the agent; supervisor_stt = an additional callee transcript; system = lifecycle markers.
one of operator, bot, supervisor_stt, system
textstringrequired