BE VOICEby Binary Elements

Integrations for agents

Integrations: agents calling your API

An integration lets your AI agent call your API while it's on a phone call: look up a job, check a balance, record a note. 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, with no adapter. Each rule below is marked Must (without it the request fails or is unsafe) or Should (it works, but calls are slower, vaguer or need more setting up). An API that follows them is what we call agent-ready.

How it works

The business connects your API to its AI agent once, in the console, with no code:

  1. A connection: your API's base address and one sign-in header.
  2. Tools: one per endpoint the agent may use. Each has a method and path, the values the agent must collect from the caller (a job number, a date), and a line saying when to use it.
  3. What the agent gets: the fields of your reply worth saying, and the business's notes on how to read them ("ON_THE_WAY means the plumber is driving there: give the arrival window").

On a call, the agent collects the values, BE Voice sends the request, and the agent words its answer from your reply. The business can instead give a fixed sentence filled from your reply, for wording that must be exact.

Caller:   "Is the plumber still coming today? The job number is HJ-1042."
Agent     → GET https://api.example.com/jobs/HJ-1042
Your API  → {"job": {"number": "HJ-1042", "status": "ON_THE_WAY", "window": "1 pm to 3 pm", "plumber": "Sam"}}
Agent:    "Yes, Sam's on the way now and should be with you between 1 and 3 this afternoon."

Setting this up in the console is covered in Connect your API to an agent.

Two ways a tool can call you

Your existing API (most integrations)
The tool calls your endpoint as it is, with GET, POST, PUT, PATCH or DELETE. Values go in the path, the query string or a JSON body, as your API expects. In the console this is Existing API · GET (read) and so on.
A BE Voice request
The tool sends one POST in BE Voice's own format, with the values and details of the call. Use it when you build an endpoint just for the agent. In the console this is Your system · BE Voice request (POST).
POST /voice/tools
Content-Type: application/json

{
  "event": "ai.tool",
  "tool": "look_up_job",
  "arguments": {"job_number": "HJ-1042"},
  "call": {"id": "cal_8f2k…", "direction": "inbound", "from": "+61400000000", "to": "+61370000000"},
  "context": {"customer_name": "Jordan"}
}

call.metadata is added when the call carries metadata, and context when the call was given facts (a campaign contact's details, or the context sent with a call your system placed). A test from the console sends "event": "ai.tool.test" with no call. A BE Voice request's path has no {name} placeholders. Everything else on this page applies to both kinds.

Address and sign-in

RuleLevel
HTTPS on a public address. Private, internal and loopback addresses are refused, including names that resolve to them.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. A redirect is treated as a failure, not followed.Must
Give BE Voice its own key, limited to what the agent needs, so it can be revoked on its own.Should
Check the signature, at least on writes (below).Should

BE Voice has no fixed source addresses, so don't rely on an allow-list of IP addresses.

Checking a request came from BE Voice

Every request carries two headers:

X-BE-Voice-Timestamp
When it was sent, in Unix seconds.
X-BE-Voice-Signature
sha256= followed by the hex HMAC-SHA256 of the timestamp, a dot, and the raw body (1791612000.{"job_number": …}), keyed with your account's webhook secret. The body is empty for a GET or DELETE, so the signed text is just the timestamp and a dot.

Compute it over the raw bytes you received (before parsing the JSON), compare in constant time, and reject timestamps more than 5 minutes old. It is the same signature as BE Voice's webhooks. Your account's webhook secret is in Settings → Integrations → Webhook secret.

import hashlib, hmac, time

def from_be_voice(headers, raw_body: bytes, secret: str) -> bool:
    ts = headers.get("X-BE-Voice-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    signed = ts.encode() + b"." + raw_body
    want = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, headers.get("X-BE-Voice-Signature", ""))

Requests

What BE Voice sends to an existing API:

PartWhat it is
MethodGET, POST, PUT, PATCH or DELETE, as the tool is set up.
PathThe tool's path with each {name} filled from what the agent collected, escaped as one path segment: /jobs/{job_number} becomes /jobs/HJ-1042. A value can't add / or change the path.
Query stringGET and DELETE: every collected value not used in the path, as ?name=value.
BodyPOST, PUT, PATCH: JSON. Either the business's body template with the values filled in ({"text": "{note}", "source": "phone"}), or, with no template, the collected values as one JSON object.
HeadersAccept: application/json, Content-Type: application/json (with a body), User-Agent: be-voice-tools/1, your sign-in header, the two signature headers, and the call headers below.

In a body template, a string that is exactly "{name}" is replaced by the value; a placeholder inside a longer string is replaced by its text. A lookup on a call looks like this:

GET /jobs/HJ-1042 HTTP/1.1
Host: api.example.com
Accept: application/json
User-Agent: be-voice-tools/1
X-Api-Key: your-key-for-be-voice
X-BE-Voice-Timestamp: 1791612000
X-BE-Voice-Signature: sha256=5d0c…
X-BE-Voice-Call-Id: cal_8f2k…
X-BE-Voice-Tool: look_up_job
Idempotency-Key: cal_8f2k…:look_up_job:3b1c9a0e5d7f2a44

Which call is this?

HeaderValue
X-BE-Voice-Call-IdThe phone call's ID (cal_…). Absent on a test from the console.
X-BE-Voice-ToolThe tool's name, for example look_up_job.
Idempotency-KeyThe call ID, the tool name and a hash of the request, joined with colons: cal_8f2k…:look_up_job:3b1c9a0e5d7f2a44. The same when the same tool sends the same request again on the same call; different otherwise. A test gets a fresh key each time (test-…).

Store the call ID with anything the agent creates, and your records can link back to the call's recording and transcript.

Rules for your API

RuleLevel
Look records up by something a caller knows or the business already holds: a job, 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 hj 1042, HJ-1042 and hj1042; 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 sends it that way. Accept free text ("Friday") only if your system can read it.Should
One request does the whole job. Don't need a lookup that returns an ID and then a second call for the details.Should

Replies

The agent reads your reply, together with the business's notes on what the values mean, and words the answer itself.

RuleLevel
JSON, UTF-8, up to 64 KB. More is a failure.Must
Statuses from a fixed, documented list of codes or words (BOOKED, ON_THE_WAY, DONE). 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. The 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' details.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 arrival window 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
{
  "job": {
    "number": "HJ-1042",
    "status": "BOOKED",
    "date": "2026-10-13",
    "window": "8 am to 10 am",
    "plumber": "Alex",
    "parts_on_order": null
  }
}

The business picks which fields reach the agent with paths such as job.status, items.0.sku or items[].name (that field of every item). Plain, stable field names make that easy.

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, then the data.

{"answer": "Job HJ-1042 is booked for Tuesday 13 October, with Alex arriving between 8 and 10 am.",
 "data": {"status": "BOOKED"}}

Status codes

Your answerWhat the agent is told
200, 201, 202 with JSONYour reply, shaped by the tool's settings.
204, or any 2xx with no body"Done."
2xx with a body that isn't JSONA failure.
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.
400, 409, 422 with no sentenceA failure.
3xx, 401, 403A failure. For 401 and 403, the connection's key is wrong.
429, 5xx, a timeoutA failure.

On a failure the agent says it couldn't check that right now and offers to take a message or arrange a call back. Nothing is retried. The business's notes on how to read the reply go with "nothing found" and refusals too, so they can say what the agent should do next.

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. It may be said to the caller, so keep it plain and free of internals.

HTTP/1.1 409 Conflict
Content-Type: application/json

{"error": "That time has just been taken. The next free times are 2:30 pm and 4 pm.", "code": "slot_taken"}

With that, the agent can offer 2:30 or 4 pm straight away instead of saying something went wrong.

Timing and limits

RuleLevel
Answer within the tool's wait: 10 seconds unless the business sets another, from 3 to 20. After that it is a failure.Must
Aim for under 2 seconds. The caller is on the line while you work; the agent says "One moment, let me check" first.Should
Per call
At most 8 tool requests on one phone call, across all the agent's tools, and one attempt each. After that the agent offers a message or a call back instead.
Per agent
Up to 10 tools.

Writes

RuleLevel
Safe to receive twice. The agent may repeat a write if the caller repeats themselves; the same note, booking or promise 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 template can send "source": "phone").Should
POST /jobs/HJ-1042/notes HTTP/1.1
Idempotency-Key: cal_8f2k…:add_job_note:9e4d1c07a2b85f31
Content-Type: application/json

{"text": "Side gate is locked: call 10 minutes before arriving.", "source": "phone"}

HTTP/1.1 201 Created
{"note": {"id": "nt_5521", "job": "HJ-1042", "text": "Side gate is locked: call 10 minutes before arriving."}}

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.

RuleLevel
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 details
On calls the business places (campaigns), the business chose who to ring and the agent confirms the person first. Still return only that contact's records.Should

Describing your API

The business sets up each tool from your documentation. For every endpoint the agent may use, give it:

You documentIt becomes
A one-line purpose, and the formats you expectThe tool's When to use line
The values the endpoint needs, named as in the path, query or bodyWhat the tool Collects from the caller
Which fields are safe and useful to sayFields to keep
Every status value, what it means, and what should happen nextThe notes on how to read the reply
An example replyA quick check with the tool's Test button

Checklist

  • HTTPS, public, one base address, one fixed sign-in header, no redirects
  • Lookups by something a caller knows; loose input accepted
  • Small JSON replies with one named record and a fixed list of statuses
  • 404 for not found; refusals as {"error": "a sentence"} with 400, 409 or 422
  • Answers in under 2 seconds, always within the tool's wait
  • Writes check the Idempotency-Key and return what was created
  • Signatures checked, at least on writes
  • Nothing in a reply that a caller mustn't hear; personal details need a second value
  • Each endpoint documented with its purpose, status values, safe fields and an example

Still stuck? Contact us and tell us which page you were on.