POST /calls
https://api.voygr.tech/calls
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.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
target_phone | string | optional | E.164 format. Example: "+15551234567" |
brief | string | null | optional | Plain-language task for the call. Required for freeform requests. |
language | string | optional | ISO 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 |
intent | string (one of inquiry, info_gathering, issue_resolution, booking, cancellation) | null | optional | Structured path only. |
slots | object | null | optional | 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 | string | optional | Optional 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_mode | string | optional | Use 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.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
call | CallDTO | required | |
credits_reserved | integer | required | Reported 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_success | integer | optional | Successful-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.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
call_id | string | required | Example: "c_abc123" |
status | string | required | one of queued, dialing, in_progress, completed, failed, cancelled |
credits_reserved | integer | required | Reported 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_success | integer | optional | Successful-outcome charge: 10 credits. Unsuccessful outcomes cost 0. Example: 10 |
status_url | string | required | Same as GET /calls/{call_id}. Example: "/calls/c_abc123" |
answer_url | string | required | Example: "/calls/c_abc123/answer" |
recording_url | string | required | Relative path /calls/{call_id}/recording. Present even before a recording exists, unlike CallDTO.recording_url. Example: "/calls/c_abc123/recording" |
replayed | boolean | optional | default False |
expected_next_steps | array of string | required |
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.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
call_id | string | required | Example: "c_abc123" |
position | integer | required | Queue position; 0 is the front. Another call may still be starting when this is 0. Example: 0 |
status | string | required | one of queued; default queued |
credits_reserved | integer | optional | Reported 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_success | integer | optional | Successful-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.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
call_id | string | required | Example: "c_abc123" |
customer_id | string | required | Example: "cus_xyz" |
target_phone | string | required | Example: "+15551234567" |
language | string | required | one of en, es, fr, de, hi, ru, pt, ja, it, nl, sr, tr, pl, auto |
status | string | required | one of queued, dialing, in_progress, completed, failed, cancelled |
started_at | string (date-time) | null | optional | |
ended_at | string (date-time) | null | optional | |
duration_sec | integer | null | optional | |
outcome_type | string (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) | null | optional | Set 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_summary | string | null | optional | |
outcome_charge_cents | integer | optional | Settled 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_at | string (date-time) | required | |
has_recording | boolean | optional | default False |
recording_url | string | null | optional | Relative recording path /calls/{call_id}/recording when a recording exists; null when has_recording is false. Recording retrieval is not documented here. |
reservation_signals | array of string | null | optional | Short free-text signals (<10 short strings) about the reservation outcome. Always included (not detail-gated like transcript_full). |
transcript_full | array of TranscriptTurn | null | optional | Only populated on GET /calls/{call_id}, not on the GET /calls list. |
TranscriptTurn
Type: object
One line of a call transcript.
| Field | Type | Required | Description and constraints |
|---|---|---|---|
ts | string (date-time) | required | |
role | string | required | operator = the callee; bot = the agent; supervisor_stt = an additional callee transcript; system = lifecycle markers. one of operator, bot, supervisor_stt, system |
text | string | required |
On this page
