Agent-ready APIs
A BE Voice AI agent can read from your system and write to it while it is on a phone call, by calling your REST API. This guide says what your API should look like so that works well: the agent finds the right record, understands the reply, says the right thing, and writes safely.
Any JSON API can be connected. Each rule below is marked Must (without it the call to your API fails or is unsafe) or Should (it works, but calls are slower, vaguer or need more setting up).
Your app can also place calls: give BE Voice a number, a goal and a brief, and its AI holds the conversation, takes your instructions while the call is on, and reports how it went. See Placing calls, or connect an AI assistant.
How it works
A business connects your API to its AI agent once, with no code:
- A connection: your API's base address and one sign-in header.
- Tools: one per endpoint the agent may use. Each has a path, the values the agent must collect from the caller (an order number, a date), and a line saying when to use it.
- What the agent gets: the fields of your reply worth saying, and the business's notes on how to read them ("BO means back-ordered: offer a call back").
On a call, the agent collects the values, BE Voice sends the request, and the agent words its answer from your reply. A business can also give a fixed sentence filled from your reply, for wording that must be exact.
Caller: "Where's my order? It's A1001."
Agent → GET https://api.yourshop.example/v2/orders/A1001
Your API → {"order": {"number": "A1001", "status": "SHP", "eta": "2026-10-07", "carrier": "Australia Post"}}
Agent: "It shipped with Australia Post and should arrive on Tuesday the 7th."
Address and sign-in
| Rule | |
|---|---|
| HTTPS on a public address. Private and internal addresses are refused. | Must |
| One base address per system; every tool is a path under it. | Must |
Sign-in is one fixed header: an API key or a long-lived token (Authorization: Bearer … or X-Api-Key: …). It is stored encrypted and sent on every request. Tokens that must be refreshed (OAuth flows) aren't supported. | Must |
| No redirects: answer on the address called. | Must |
| Give BE Voice its own key, limited to what the agent needs, so it can be revoked alone. | Should |
| Check the signature on writes (below). | Should |
Checking a request came from BE Voice
Every request carries X-BE-Voice-Timestamp (Unix seconds) and X-BE-Voice-Signature: sha256= followed by the hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with the business's webhook secret. The body is empty for a GET. Compute it over the raw bytes and reject timestamps more than 5 minutes old. BE Voice has no fixed source addresses to allow-list.
Requests
| What BE Voice sends | |
|---|---|
| Methods | GET, POST, PUT, PATCH, DELETE |
| Path | The tool's path with {name} filled from what the agent collected, each value escaped as one path segment: /orders/{order_number} becomes /orders/A1001 |
| Query string | GET and DELETE: every collected value not used in the path, as ?name=value |
| Body | POST, PUT, PATCH: JSON, from a template the business sets ({"text": "{note}", "source": "phone"}), or the collected values as one JSON object |
| Headers | Accept: application/json, Content-Type: application/json (with a body), User-Agent: be-voice-tools/1, your sign-in header, the signature headers, and the call headers below |
Which call is this?
| Header | Value |
|---|---|
X-BE-Voice-Call-Id | The phone call's ID (cal_…). Absent on a test from the console. |
X-BE-Voice-Tool | The tool's name, e.g. get_order |
Idempotency-Key | <call id>:<tool>:<hash of the request>. The same when the same tool sends the same request again on the same call; different otherwise. Tests get a fresh key each time (test-…). |
Rules for your API
| Rule | |
|---|---|
| Look records up by something a caller knows or the business already holds: an order or invoice number, a phone number, a name with a postcode. Not an internal database ID, unless your system gave it to BE Voice with the call. | Must |
| Ignore fields and headers you don't know. | Must |
Take values as people say them. They arrive as text the agent heard: accept inv 881, INV-881 and inv881; ignore case and spaces in identifiers. | Should |
Dates and amounts: accept ISO dates (2026-10-10) and plain amounts (500, 500.00). Tell the business which format you need; it goes in the tool's "when to use" line, so the agent is told to send it that way. | Should |
| One request does the whole job. Don't need two calls where one could answer. | Should |
Replies
The agent reads your reply, together with the business's notes on what the values mean, and words the answer itself.
| Rule | |
|---|---|
| JSON, UTF-8, up to 64 KB. More is a failure. | Must |
Statuses from a fixed, documented list of codes or words (SHP, BO, CAN). The business writes one line per value for the agent, so the list must be stable and complete. | Must |
| Nothing the caller mustn't hear. A business can drop fields before the agent sees them, but the safest secret is one the reply never had: no card numbers, passwords or other customers' data. | Must |
| Small. What reaches the agent is cut at 2,000 characters. Return one record, or a short list (10 items or fewer) with a way to narrow it. | Should |
| One record under a named key, with plain field names. | Should |
| Answer the next question in the same reply: the delivery date with the status, the balance with the amount, the payment link with the invoice. | Should |
Unknown is null, not "", 0 or "N/A". | Should |
Dates as ISO 8601 with an offset, or as plain words; not Unix timestamps. Money as a number in a known currency, or as text ("$1,240.50"). | Should |
| No HTML or markup in text fields. | Should |
{
"order": {
"number": "A1002",
"status": "BO",
"eta": null,
"restock_date": "2026-10-13",
"items": [{"name": "Tap washer kit", "qty": 3}]
}
}
Optional: word the answer yourself
If your system can say it better than a general assistant would, return a top-level "answer" sentence with the record under "data". The agent is given your sentence first.
{"answer": "Order A1002 is waiting on stock, due back in on Monday 13 October.", "data": {"status": "BO"}}
Status codes
| Your answer | What the agent is told |
|---|---|
200, 201, 202 with JSON | Your reply |
204, or 2xx with no body | "Done." |
404 | "Nothing was found", with your body. Use it for "no such record", never 200 with an empty body. |
400, 409, 422 with a sentence | "The system refused that: your sentence". The agent fixes the value and tries again, or tells the caller. |
401, 403 | A failure. The connection's key is wrong. |
429, 5xx, a timeout, not JSON | A failure. The agent says it couldn't check and offers a message or a call back. Nothing is retried. |
Errors the agent can act on
Send refusals as a sentence a person could act on. It is read from error (a string, or an object with message), or else message or detail, up to 300 characters, and it may be said to the caller, so keep it plain and free of internals. A refusal with no sentence is treated as a failure.
HTTP 409
{"error": "That time has just been taken. The next free times are 2:30 pm and 4 pm.", "code": "slot_taken"}
Timing and limits
| Rule | |
|---|---|
| Answer within the tool's wait: 10 seconds by default; a business can set 3 to 20. After that it is a failure. | Must |
| Aim for under 2 seconds. The caller is on the line while you work. | Should |
| At most 8 tool requests per phone call, across all tools, and one attempt each. |
Writes
| Rule | |
|---|---|
Safe to receive twice. The agent may repeat a write if the caller repeats themselves; the same promise, note or booking must not double up. Check the Idempotency-Key. | Must |
Check the values yourself. They are what the agent heard; refuse what makes no sense with 422 and a sentence. | Must |
| Return what was created, with its ID and the values as stored. The agent reads them back to the caller. | Should |
Make the agent's writes undoable by staff, and mark them as coming from the phone agent (the body can carry "source": "phone"). | Should |
POST /invoices/INV-881/promises
Idempotency-Key: cal_8f2…:record_promise_to_pay:3b1c9a0e5d7f2a44
{"amount": "500", "date": "2026-10-10", "source": "phone"}
HTTP 201
{"promise": {"id": "pr_1042", "invoice": "INV-881", "amount": "500.00", "date": "2026-10-10"}}
Who is on the line
BE Voice can't prove that a caller is who they say. An API that returns personal or account details makes that decision itself.
| Rule | |
|---|---|
For details only the customer should hear, require a second value the caller gives (a postcode, a date of birth, the amount of the last invoice) with the lookup, and answer 404 unless both match. | Must for personal data |
| On calls the business places, the business chose who to ring and the agent confirms the person first. Still return only that contact's records. | Should |
Describing your API
A business sets up each tool from your documentation. Give it, for every endpoint the agent may use:
| You document | It becomes |
|---|---|
| A one-line purpose, and the formats you expect | The tool's "when to use" line |
| The values the endpoint needs, named as in the path, query or body | What the agent collects from the caller |
| Which fields are safe and useful to say | The fields the agent is given |
| Every status value, what it means, and what should happen next | The agent's notes on how to read the reply |
| An example reply | A quick test when connecting |
Starting the AI from your system
The same connection works the other way: your system can have the AI place calls and hear how they went.
| To | Your system |
|---|---|
| Have the AI call one person | Starts a call through the BE Voice API, naming the agent and giving the facts for this call. The result (outcome, details, a note, the transcript) arrives at your webhook when the call ends. |
| Have it work through a list | Provides two endpoints: one returns the people to call with the facts for each; the other receives each person's result, and a final summary when the run is finished. |
| Trust what arrives | Checks the signature, as above, and treats the call's ID as the key for repeats. |
Put the IDs your endpoints need (an invoice number, an appointment ID) in the facts you send with each call, so the agent never has to ask the person for them. The full API reference is available to customers.
Checklist
- HTTPS, public, one base address, one fixed sign-in header
- Lookups by something a caller knows; loose input accepted
- Small JSON replies with one named record and a fixed list of statuses
404for not found; refusals as{"error": "a sentence"}with400,409or422- Answers in under 2 seconds, always within 10
- Writes check the
Idempotency-Keyand return what was created - Nothing in a reply that a caller mustn't hear; personal data needs a second value
- Each endpoint documented with its purpose, status values, safe fields and an example
To check your API, the business adds a connection and a tool in its BE Voice console and presses Test with real values. It shows your reply as it came, and exactly what the agent would be told.
Place a call
Send the number, the goal and your brief. Nothing needs setting up in BE Voice first: the AI works from what you send, opens the call in its own words (or with your greeting), and finishes when the goal is met or clearly can't be. Calls come from one of your account's numbers.
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",
"prompt": "You are Alex from Acme Parts dispatch. Offer Friday 8-12 or 12-5; if neither works, find a day that does.",
"context": {"customer_name": "Sam", "order": "A1002"},
"questions": [{"key": "window", "ask": "Which window suits you?", "type": "choice",
"choices": ["Friday 8-12", "Friday 12-5", "Other"], "required": true}],
"human": {"target": "ext:101"}
},
"callback_url": "https://app.example.com/hooks/be-voice",
"metadata": {"ref": "ord_A1002"}
}'
| Field | What it does |
|---|---|
task.goal | What counts as done. Required. |
task.prompt | Your brief for the AI: who it is, who it calls for, what it may say or offer. |
task.context | Facts for this call (names, order numbers), so the AI never has to ask for them. |
task.questions | Questions to ask in order. Each answer is checked against its type (text, number, yes_no, choice, date) and returned under its key. |
task.human | Where the call goes when the person asks for a human: an extension (ext:101) or a phone number, with an optional then if nobody answers. |
task.outcomes | How a call can end. Default: achieved, not_achieved. |
task.greeting | What the AI says first: after their hello, or first if nobody speaks. |
task.closing, voice, voicemail_message | Optional wording and voice. |
from | Optional: one of your numbers. Default: your first. |
callback_url | Optional: HTTPS address that receives the result when the call is done. |
The reply is the call, with its id. If the person asks not to be called again, their number goes on your account's do-not-call list, and calls to numbers on that list are refused with do_not_call.
When the person answers with "hello", the AI waits until they finish, then says your greeting. If nobody speaks, it speaks first. Without a greeting, it opens in its own words.
Steer a call
While the call is on, you can give the AI a new instruction. The person doesn't hear it; the AI acts on it at the next natural moment, and it shows in the transcript as an instruction line.
curl https://voice.binaryelements.com/v1/calls/$CALL_ID/steer \
-H "Authorization: Bearer $BE_VOICE_KEY" \
-d '{"instruction": "Also ask whether Saturday works."}'
409 means there is no conversation to steer right now: the phone is still ringing, the call is being transferred, or it has ended.
Get the result
Ask for it, optionally waiting until the call is done (wait in seconds), or let it come to your callback_url as a call.result webhook, signed as described above.
curl "https://voice.binaryelements.com/v1/calls/$CALL_ID/result?wait=60" -H "Authorization: Bearer $BE_VOICE_KEY"
{
"call_id": "cal_…", "status": "completed", "final": true, "duration_sec": 74,
"outcome": "completed",
"result": {"outcome": "completed", "answers": {"window": "Friday 8-12"}, "note": "Gate code 4411."},
"summary": "Sam will take delivery Friday morning.",
"transcript": [{"speaker": "agent", "text": "…"}, {"speaker": "caller", "text": "…"}],
"metadata": {"ref": "ord_A1002"}
}
| Field | Meaning |
|---|---|
final | false while the call is on (the transcript so far) and for a few seconds after, while the summary is written. |
outcome | The AI's outcome; or, if nobody answered, no-answer, busy, failed or canceled. |
result | Answers by question key, details, the AI's note, opt_out if they asked not to be called. |
Connect an AI assistant
BE Voice is also an MCP server, so an AI assistant can place calls, follow them, steer them and read the results itself. Add it to any MCP client that supports remote servers:
{
"mcpServers": {
"be-voice": {
"type": "http",
"url": "https://voice.binaryelements.com/mcp",
"headers": { "Authorization": "Bearer YOUR_BE_VOICE_KEY" }
}
}
}
| Tool | Does |
|---|---|
place_call | Calls a number with a goal and a brief; returns the call id |
wait_for_call | Waits for the call to finish and returns the result |
get_call | The call now: status, live transcript, result |
steer_call | A new instruction for the AI on the call |
hang_up | Ends the call |
list_caller_numbers, list_voices, list_recent_calls | What calls can come from, the voices, recent calls |
Then just ask: "Call Sam on +61 400 000 000 and confirm Friday morning works for the delivery; tell me what they say." Placing calls through the API and MCP is switched on per account: ask Binary Elements.
Nothing matches that search.