BrightshoalDocsOpen console
MenuPlace an outbound call

Voice

Place an outbound call

Have an assistant call a phone number from your system, personalise the call with variables, then read back the transcript, recording and outcome.

An outbound call is one request: the assistant and the number to call. The platform opens a voice room, dials the number over the phone network and joins the assistant to the call. Your server hears how it went from the call record or from webhooks.

You need a secret API key, an assistant (the quickstart creates one) and telephony set up on your account. If it isn't, the request answers 503 with livekit_not_configured; ask us to switch it on.

Place the call

to is the number to call in E.164 format: a +, the country code, then the number, no spaces.

curl -X POST https://api.myleadsfactory.tech/api/calls \
  -H "Authorization: Bearer $MLF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assistant_id": "asst_7d2e9b14c6a04f3e8b5d1a9c0e7f6b23",
    "to": "+14155550123"
  }'

With the default voice provider the request stays open while the phone rings and returns once the call is answered, so give it a generous timeout (a minute or more). If nobody answers or the dial fails, it answers 502 provider_error and the call is marked failed. The success body still reports status as ringing, and it is not wrapped in data:

Response 201
{
  "call_id": "call_5e1a7c9d2b4f4e8a9c3d6b0f1e2a4c7d",
  "room": "out_call_5e1a7c9d2b4f4e8a9c3d6b0f1e2a4c7d",
  "status": "ringing"
}

Keep call_id: everything else about the call hangs off it.

Personalise it

Write {{ placeholders }} in the assistant's instructions, context or first_message, declare them in config.variables, and fill them per call. Values are strings up to 2,000 characters; keys start with a letter or underscore.

Request body
{
  "assistant_id": "asst_7d2e9b14c6a04f3e8b5d1a9c0e7f6b23",
  "to": "+14155550123",
  "variables": { "first_name": "Ana", "trip": "Lisbon, 12 to 19 May" },
  "metadata": { "crm_contact_id": "0035e00001Xyz" }
}

A placeholder with no value and no default is listed in unresolved_variables in the response, so you can catch a missing field before it reaches a customer. metadata is yours: it is stored on the call and returned when you read it, which is the easy way to join a call to the record in your own system.

To show a particular caller ID, pass from: one of your numbers, active in the same project as the assistant (anything else answers 404 not_found). A number in test mode only places test calls: a live call from it answers 409 test_number_requires_test_mode. Leave it out and the call goes out from the number configured on your outbound line.

Follow the call

The call moves through ringing, in_progress and then one final status: completed, failed, no_answer, busy or cancelled. To be told rather than ask, subscribe to call.started, call.connected and call.ended webhooks. call.ended carries the transcript, duration, usage and an ended_reason such as customer_hangup, assistant_ended, voicemail, no_answer or transferred.

To end a call early, use POST/api/calls/{id}/hangup. It answers {"data":{"id":"call_...","hangup_requested":true}}, or 409 call_not_active once the call is over.

Read it back

curl https://api.myleadsfactory.tech/api/calls/call_5e1a7c9d2b4f4e8a9c3d6b0f1e2a4c7d \
  -H "Authorization: Bearer $MLF_API_KEY"

The call record holds everything that happened:

FieldWhat it holds
status, ended_reasonWhere the call is now, and why it ended
transcriptEach turn with role and content (null until there is one)
analysissummary and structured results once post-call analysis has run
recordingsRecording metadata; fetch audio separately (below)
costsUsage lines (tokens, audio seconds) per provider
performanceLatency measurements, when the voice runtime reported them
eventsThe call's lifecycle events, newest last

Get the recording

List recordings with GET/api/calls/{id}/recordings, then ask for a short-lived link to one:

Terminal
curl "https://api.myleadsfactory.tech/api/calls/$CALL_ID/recordings/$RECORDING_ID/access?download=true" \
  -H "Authorization: Bearer $MLF_API_KEY"
Response
{ "data": { "url": "https://...", "expires_at": "2026-10-28T09:45:00.000Z" } }

The link works for 15 minutes. Fetch it from your server and store the audio yourself if you need to keep it; don't save the link. A recording that is still processing answers 409 recording_not_ready.

Find calls later

GET/api/calls lists calls newest first, 50 at a time (up to 200 with limit). Filter with assistant_id, status, call_type (inbound, outbound or web), from and to (ISO timestamps) or q (matches the call ID, either number or the assistant ID), and follow next_cursor for the next page: Pagination.

When it goes wrong

StatusErrorWhat to do
400invalid_requestRead issues: usually to is not E.164 or a variable key is invalid
404not_foundThe assistant, or the from number, is not in your key's project
502provider_errorThe call was not answered, or the phone network refused the dial. The call is marked failed; retry later
503livekit_not_configuredTelephony is not set up for your account

Listen in

For a live call, POST/api/calls/{id}/listen returns { token, url, room }: a listen-only token for the call's voice room. Join it with a LiveKit client SDK to hear the call as it happens, without being heard.

Stuck on place an outbound call?