DocsPlace a call

Get started · 3 of 4

Place a call

One request places a phone call. The API checks everything it can before the phone rings, answers with the call it created, and sends the result to your webhook when the call is over.

The request

POST /v1/calls needs two things: the agentId to use and the number to dial in to.

Write the number in international format: a plus sign, the country code, then the number. Spaces, hyphens and parentheses are removed for you, so +91 98765 43210 is accepted. What remains must be + followed by 8 to 15 digits.

A 202 means the provider has accepted the call and the phone is about to ring. It does not mean anyone has answered.

POST /v1/callscURL
curl --fail-with-body "http://localhost:4000/v1/calls" \  -X POST \  -H "Authorization: Bearer $OXUMI_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "agentId": "81ee2426-7abe-444b-96f8-b8e195ddde49",    "to": "+919876543210",    "variables": {      "driver_name": "Ramesh",      "company": "Shakti Logistics",      "route": "Nagpur to Hyderabad",      "planned_eta": "tonight at 9 pm"    },    "metadata": {      "trip_id": "TRIP-2291"    }  }'
202ResponseExample
{  "id": "0b9f6c1e-52a7-4d1c-9b6e-3f2a8d4c7e10",  "projectId": "bf98c165-b0d8-4006-91b9-0c97df86ba15",  "agentId": "81ee2426-7abe-444b-96f8-b8e195ddde49",  "channel": "phone",  "model": "anthropic:claude-haiku-5-5",  "variables": {    "driver_name": "Ramesh",    "company": "Shakti Logistics",    "route": "Nagpur to Hyderabad",    "planned_eta": "tonight at 9 pm"  },  "fields": {},  "outcome": null,  "status": "ringing",  "error": null,  "to": "+919876543210",  "metadata": {    "trip_id": "TRIP-2291"  },  "telephony": "twilio",  "providerCallId": "CA5f0c2b6d1e9a4c7f8b3d2e1a0c9f8e7d",  "startedAt": "2026-10-12T14:02:11.412Z",  "endedAt": null}

Checks before dialling

These run in order, and the first that fails is the error you get. Nothing is dialled unless all of them pass.

#CheckIf it fails
1agentId is a string400
2to is a valid number400
3The agent exists in the key's project404
4The agent's flow has no errors422
5Every flow variable has a value400
6The server can receive provider callbacks503
7A telephony provider is connected400
8The model's provider is connected400
9A speech provider is connected400
10The telephony provider accepts the call400

Note · Check 10 happens after the call is recorded

If the provider refuses the call, the response is 400 with the provider's message, the record is closed with outcome failed, and a call.completed event is still sent.

400ResponseCaptured
{  "message": "to must be a phone number in international format, e.g. \"+919876543210\".",  "error": "Bad Request",  "statusCode": 400}
422ResponseCaptured
{  "message": "This agent's flow is not ready: \"ask_question\" is not connected to a next step.",  "error": "Unprocessable Entity",  "statusCode": 422}
503ResponseCaptured
{  "message": "Phone calls are not available: the server has no public URL configured (PUBLIC_URL).",  "error": "Service Unavailable",  "statusCode": 503}

Variables

A flow can declare variables, such as the driver's name, that its lines use as {{driver_name}}. They are listed in flow.variables on GET /v1/agents/:id.

  • Every declared variable needs a non-empty value, or the call is refused.
  • Values are stored as strings. A dropped value (see Metadata, below) counts as missing.
  • Keys the flow does not declare are stored with the call and otherwise ignored.
400A variable is missingCaptured
{  "message": "Missing variables: driver_name, company, route, planned_eta",  "error": "Bad Request",  "statusCode": 400}

Metadata

metadata is yours. Put in whatever lets you match the result to your own records, such as an order or trip id. It is stored with the call and returned by GET /v1/calls/:id and in the webhook. The agent never sees it.

It is a flat object, stored by the same rules as variables.

How variables and metadata values are stored
string
Kept as it is
number
Converted to a string
boolean
Converted to "true" or "false"
null
Dropped
array, object
Dropped

Provider and model

Both are optional. Leave them out and the agent's and the project's own settings decide, in the order shown.

  • A provider that was named, by the request or by the agent, and is not fully connected is a 400. There is no fallback to the other one.
  • The voice comes from the agent's speech setting, and the number the call comes from is the agent's phoneNumber or else the provider's default. Neither can be changed per call.
Which telephony provider places the call
1. provider
The request's own, twilio or exotel
2. agent.telephony
The agent's setting, if not empty
3. Twilio
If the project has it connected
4. Exotel
Otherwise
Which language model runs the call
1. model
The request's own, e.g. "anthropic:claude-haiku-5-5"
2. agent.model
The agent's setting, if not empty
3. Default
The project's default model

After the request

StatusMeaning
queuedRecorded, not yet accepted by the provider.
ringingAccepted by the provider. This is what the 202 returns.
in_progressAnswered. The agent is running the flow.
endedOver. outcome and endedAt are set.

Wait for the webhook rather than polling. If you do poll, GET /v1/calls/:id returns the call with its turns so far.

A call cannot run longer than the flow's time limit, flow.settings.maxDurationSec (300 seconds unless the flow sets another). The provider ends it at that point.

GET /v1/calls/dddddddd-0000-4000-8000-000000000007cURL
curl --fail-with-body "http://localhost:4000/v1/calls/dddddddd-0000-4000-8000-000000000007" \  -H "Authorization: Bearer $OXUMI_API_KEY"
200ResponseCaptured
{  "id": "dddddddd-0000-4000-8000-000000000007",  "projectId": "bf98c165-b0d8-4006-91b9-0c97df86ba15",  "agentId": "81ee2426-7abe-444b-96f8-b8e195ddde49",  "agent": {    "id": "81ee2426-7abe-444b-96f8-b8e195ddde49",    "name": "Driver check-in"  },  "channel": "browser_voice",  "model": "anthropic:claude-haiku-5-5",  "variables": {    "route": "Kolkata to Patna",    "company": "Shakti Logistics",    "driver_name": "Deepak",    "planned_eta": "tomorrow at 8 am"  },  "fields": {    "current_location": "Asansol"  },  "outcome": null,  "status": "in_progress",  "error": null,  "to": null,  "metadata": {},  "telephony": null,  "providerCallId": null,  "startedAt": "2026-10-12T21:38:51.218Z",  "endedAt": null,  "turns": [    {      "id": "c5ec414c-d122-4955-bb36-f980ee361f75",      "callId": "dddddddd-0000-4000-8000-000000000007",      "index": 0,      "callerText": null,      "agentText": "नमस्ते दीपक जी, मैं शक्ति लॉजिस्टिक्स की ओर से ऑटोमेटेड असिस्टेंट बोल रही हूँ। आपकी कोलकाता से पटना ट्रिप का एक छोटा सा अपडेट चाहिए था। अभी आप कहाँ हैं?",      "interrupted": false,      "nodeId": "greeting",      "llmMs": null,      "audioMs": 337,      "createdAt": "2026-10-12T21:38:51.218Z"    },    {      "id": "25b47bb5-81a8-4e96-984f-dcfbd3c58630",      "callId": "dddddddd-0000-4000-8000-000000000007",      "index": 1,      "callerText": "आसनसोल पहुँच गया हूँ।",      "agentText": "ठीक है, आसनसोल। आपको कल सुबह 8 बजे तक पहुँचना है। क्या समय पर पहुँच जाएँगे?",      "interrupted": false,      "nodeId": "ask_schedule",      "llmMs": 409,      "audioMs": 761,      "createdAt": "2026-10-12T21:39:06.218Z"    }  ]}

When nobody answers

A call that never reaches a person still ends, and still produces a call.completed event. Its outcome is failed, turns is empty, and error says what happened.

errorThe provider reported
The line was busy.busy
Nobody answered.no-answer
The call could not be connected.failed
The call was cancelled before it was answered.canceled
The call ended before the conversation started.completed, with no conversation

Note · No automatic redial

The API does not call again by itself. To retry, place a new call when your system decides to, for example after the webhook for the failed one arrives.

data of the call.completed eventExample
{  "call": {    "id": "0b9f6c1e-52a7-4d1c-9b6e-3f2a8d4c7e10",    "agentId": "81ee2426-7abe-444b-96f8-b8e195ddde49",    "agentName": "Driver check-in",    "channel": "phone",    "to": "+919876543210",    "status": "ended",    "outcome": "failed",    "error": "Nobody answered.",    "fields": {},    "variables": {      "driver_name": "Ramesh",      "company": "Shakti Logistics",      "route": "Nagpur to Hyderabad",      "planned_eta": "tonight at 9 pm"    },    "metadata": {      "trip_id": "TRIP-2291"    },    "model": "anthropic:claude-haiku-5-5",    "startedAt": "2026-10-12T14:02:11.412Z",    "endedAt": "2026-10-12T14:02:43.006Z",    "durationSeconds": 32,    "turns": []  }}

Hanging up

DELETE /v1/calls/:id ends a call that is ringing or in progress and answers 204. The result still arrives by webhook.

  • It works for calls placed through Twilio. For an Exotel call it returns 400, because Exotel offers no way to end a call through its API; the call ends at the flow's time limit.
  • For a call that has already ended, the response is 204 and nothing happens.
DEL /v1/calls/0b9f6c1e-52a7-4d1c-9b6e-3f2a8d4c7e10cURL
curl --fail-with-body "http://localhost:4000/v1/calls/0b9f6c1e-52a7-4d1c-9b6e-3f2a8d4c7e10" \  -X DELETE \  -H "Authorization: Bearer $OXUMI_API_KEY"