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:
{
"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.
{
"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:
| Field | What it holds |
|---|---|
status, ended_reason | Where the call is now, and why it ended |
transcript | Each turn with role and content (null until there is one) |
analysis | summary and structured results once post-call analysis has run |
recordings | Recording metadata; fetch audio separately (below) |
costs | Usage lines (tokens, audio seconds) per provider |
performance | Latency measurements, when the voice runtime reported them |
events | The 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:
curl "https://api.myleadsfactory.tech/api/calls/$CALL_ID/recordings/$RECORDING_ID/access?download=true" \
-H "Authorization: Bearer $MLF_API_KEY"{ "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
| Status | Error | What to do |
|---|---|---|
| 400 | invalid_request | Read issues: usually to is not E.164 or a variable key is invalid |
| 404 | not_found | The assistant, or the from number, is not in your key's project |
| 502 | provider_error | The call was not answered, or the phone network refused the dial. The call is marked failed; retry later |
| 503 | livekit_not_configured | Telephony 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.