For developers
Place an AI call from your app
Send BE Voice a number, a goal and a brief. It rings the number from one of your business's numbers, and its AI holds the conversation until the goal is met or clearly can't be. Nothing needs setting up in the console first.
How to place a call
- Get an API key and check your account includes Calls through the API and MCP. See API keys and basics.
- Send
POST /v1/callswithtoand ataskthat has at least agoal. - Keep the
idfrom the reply (cal_…). The phone is already ringing. - Follow the call and read its result with
GET /v1/calls/{id}/result, or have the result sent to yourcallback_url. See Follow, steer and get results.
curl https://voice.binaryelements.com/v1/calls \
-H "Authorization: Bearer $BE_VOICE_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"to": "+61400000000",
"task": {"goal": "Confirm Sam can take delivery of order A1002 on Friday morning"}}'
A full example
Harbour Plumbing's job system asks a customer which delivery window suits, and sends the answer back to itself.
curl https://voice.binaryelements.com/v1/calls \
-H "Authorization: Bearer $BE_VOICE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: delivery-A1002" \
-d '{
"to": "+61400000000",
"from": "+61370000000",
"task": {
"goal": "Confirm Sam can take delivery of order A1002 on Friday, and in which window",
"prompt": "You are Alex from Harbour Plumbing in Port Melbourne. Sam ordered a hot water unit (order A1002). Offer Friday 8-12 or 12-5. If neither works, find a weekday next week that does. Be brief and friendly.",
"context": {"customer_name": "Sam", "order": "A1002", "item": "hot water unit"},
"questions": [
{"key": "window", "ask": "Which delivery window suits you?", "type": "choice",
"choices": ["Friday 8-12", "Friday 12-5", "Another day"], "required": true},
{"key": "other_day", "ask": "Which day suits instead?", "type": "date"},
{"key": "gate_code", "ask": "Is there a gate code or anything the driver should know?", "type": "text"}
],
"human": {"target": "ext:101", "then": "+61370000000"},
"greeting": "Hi, it is Alex from Harbour Plumbing, about your hot water delivery.",
"closing": "Thanks Sam, see you then.",
"voicemail_message": "Hi Sam, it is Alex from Harbour Plumbing about your delivery. Please call us back on 03 7000 0000.",
"max_call_sec": 300
},
"callback_url": "https://app.example.com/hooks/be-voice",
"metadata": {"order": "A1002", "job": "job_48213"}
}'
The reply is the new call, with 201 Created:
{
"id": "cal_7h2kq9m4x8pz3vd61wtn",
"tenant_id": "tnt_8f2kd0w3m5q7r9t1v4xz",
"direction": "outbound",
"from": "+61370000000",
"to": "+61400000000",
"cli_presented": "+61370000000",
"case_id": "",
"status": "queued",
"metadata": {"order": "A1002", "job": "job_48213"},
"duration_sec": 0,
"created_at": "2026-10-10T05:12:03.418Z",
"answered_at": null,
"ended_at": null
}
status is queued while the phone rings. tenant_id is your account ID. Sending the same request again with the same Idempotency-Key returns this call with 200 instead of ringing Sam twice.
The request
| Field | Type | What it does |
|---|---|---|
to | string | Required. The number to ring, in E.164 (+61400000000). |
task | object | Required for an AI call: the goal and the brief (below). |
from | string | Optional. The caller ID: one of your business's numbers that calls may come from. Left out, your first such number is used. The MCP tool list_caller_numbers lists them. |
callback_url | string | Optional. An https address on the public internet (up to 500 characters) that receives the result as a signed call.result webhook once the call is over. See Webhooks. |
metadata | object | Optional. Your own references, up to 4 KB of JSON. It is returned with the call and its result, untouched. The AI doesn't see it: put facts for the conversation in task.context. |
timeout_sec | integer | Optional. How long the phone rings before the call counts as unanswered: 5 to 120 seconds, default 30. |
The task: the call's brief
Only goal is required. Everything else shapes how the AI goes about it.
| Field | Type | What it does |
|---|---|---|
goal | string | Required, up to 1,000 characters. What counts as done. The AI ends the call politely when the goal is met, or when it clearly can't be met on this call. |
prompt | string | Up to 8,000 characters. Your brief: who the AI is, who it's calling for, what it may say or offer, how to handle questions and objections. It's the AI's instructions for the whole call. |
context | object | Facts for this call (names, order numbers, amounts, times), up to 8 KB of JSON. The AI is shown them as given, so it never has to ask for something you already know. |
questions | array | Up to 30 questions, asked one at a time in order. Each answer is checked against its type and returned under its key. See Questions. |
outcomes | array | How the call can end, up to 20. See Outcomes. |
human | object | Where the call goes when the person asks for a real person. See Talking to a person. |
transfers | array | Other places the AI may put the person through to, each {"name", "when", "target", "then", "ring_sec"}. name is what the person would ask for ("Accounts"), when says when to use it in plain words. |
greeting | string | What the AI says first. If the person answers with "hello", it waits until they finish, then says it. If nobody speaks within about 2.5 seconds, it says it first. Left out, the AI opens in its own words at that same moment. |
closing | string | The goodbye line it ends with. |
voicemail_message | string | The message the AI leaves if the call reaches voicemail or an answering machine. Left out, it hangs up without leaving one. |
business_name | string | The business the AI calls for. Default: your business's name. |
summary_instructions | string | How to write the after-call summary. Default: two or three sentences on whether the goal was met, what was agreed and any follow-up. |
voice | string | A voice ID from GET /v1/ai/voices (or the MCP tool list_voices). Default: the standard voice. |
speed | number | Speaking speed, 0.8 to 1.15. Default 0.97. |
stability | number | 0.2 to 0.9: lower sounds more expressive, higher more even. |
language | string | The language spoken and understood, such as en-AU (the default). |
timezone | string | The time zone the AI uses for "today" and times, such as Australia/Brisbane. Default Australia/Melbourne. |
max_call_sec | integer | The longest the conversation may last, 30 to 3,600 seconds. It can't be longer than your account's longest call. |
Questions
Each question is an object:
key- Required. The answer's name in the result: lower-case letters, digits and
_, starting with a letter (gate_code), unique in the call. ask- Required, up to 300 characters. The question as you'd ask it. The AI asks it in its own words and comes back to it if the answer doesn't fit.
typetext(the default, up to 300 characters),number,yes_no,choiceordate(returned as YYYY-MM-DD).choices- For
choiceonly: 2 to 12 options, each up to 60 characters. The answer is one of them. requiredtrueif the call isn't complete without it. If the person skips it, the AI asks once more.
Outcomes
The AI ends every call with one outcome, returned as outcome in the result. Without your own list, the outcomes are achieved and not_achieved, or with questions completed (every required question answered), partial and not_achieved.
To use your own, send outcomes: each has a name (lower-case, like a key), a description of when it applies (up to 300 characters), and optionally up to 10 fields, details the AI records with that outcome (returned in result.details). If you send questions as well, your list must include completed and partial.
"outcomes": [
{"name": "booked", "description": "they chose a delivery time", "fields": ["day", "window"]},
{"name": "call_back", "description": "they asked us to call another time"},
{"name": "not_achieved", "description": "they couldn't or wouldn't arrange delivery (say why in the note)"}
]
Talking to a person
human is where the call goes if the person asks for a human: {"target": "ext:101", "then": "+61370000000", "ring_sec": 20}.
targetis one of your extensions (ext:101), a ring group or queue (ring_group:rg_…,queue:q_…), or a phone number.thenis tried if the first doesn't answer.ring_secis how long each rings, 5 to 120 seconds (default 20).- A transfer to a phone number goes out from the call's own number.
- Without
human, the AI tells the person nobody is available and carries on.
When a call is refused
A refused call isn't placed, and nobody is rung. The reply is an error with a code:
| Code | Why |
|---|---|
do_not_call (422) | The number is on your account's do-not-call list. The message says so: "+61400000000 is on this account's do-not-call list". |
emergency_not_allowed (422) | Emergency numbers (000, 112, 106) can never be called through the API. |
invalid_to, destination_not_allowed (422) | to isn't a valid number, or not one your account can call. |
invalid_from, cli_not_allowed (422) | from isn't one of your numbers that calls may come from, or your account has none yet. |
invalid_task (422) | Something in task is wrong; the message names the field ("task.goal is required"). |
invalid_callback_url, invalid_metadata, invalid_timeout (422) | The field named isn't valid. |
idempotency_mismatch (422) | The Idempotency-Key was already used with a different request. |
tenant_busy (429) | Your account already has as many calls going out at once as it allows. |
module_off (403) | Your account doesn't include Calls through the API and MCP. Contact us to add it. |
Rules that still apply
BE Voice adds very little to your brief: what the AI says is up to your prompt. These still apply to every call:
- Do-not-call. If the person asks not to be called again, the AI adds their number to your account's do-not-call list, and the result has
"opt_out": true. Later calls to that number, from your app, an AI assistant or a campaign, are refused. You can see and edit the list in the console (see Campaign results). - Your numbers only. Calls show one of your business's numbers. Emergency numbers are never called, and premium 190 numbers can't be transfer targets.
- Calls at once. Your account's limit on calls going out at the same time applies.
- Calling hours are yours to keep. An API call rings as soon as you place it, whatever the time. Unlike campaigns, it has no calling window, so your app decides when it's right to call, and your prompt should have the AI say who it is and who it's calling for, as the rules for your kind of call require.
