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.
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" } }'{ "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.
| # | Check | If it fails |
|---|---|---|
| 1 | agentId is a string | 400 |
| 2 | to is a valid number | 400 |
| 3 | The agent exists in the key's project | 404 |
| 4 | The agent's flow has no errors | 422 |
| 5 | Every flow variable has a value | 400 |
| 6 | The server can receive provider callbacks | 503 |
| 7 | A telephony provider is connected | 400 |
| 8 | The model's provider is connected | 400 |
| 9 | A speech provider is connected | 400 |
| 10 | The telephony provider accepts the call | 400 |
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.
{ "message": "to must be a phone number in international format, e.g. \"+919876543210\".", "error": "Bad Request", "statusCode": 400}{ "message": "This agent's flow is not ready: \"ask_question\" is not connected to a next step.", "error": "Unprocessable Entity", "statusCode": 422}{ "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.
{ "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.
- 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
phoneNumberor else the provider's default. Neither can be changed per 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
- 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
| Status | Meaning |
|---|---|
queued | Recorded, not yet accepted by the provider. |
ringing | Accepted by the provider. This is what the 202 returns. |
in_progress | Answered. The agent is running the flow. |
ended | Over. 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.
curl --fail-with-body "http://localhost:4000/v1/calls/dddddddd-0000-4000-8000-000000000007" \ -H "Authorization: Bearer $OXUMI_API_KEY"{ "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.
| error | The 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.
{ "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
204and nothing happens.
curl --fail-with-body "http://localhost:4000/v1/calls/0b9f6c1e-52a7-4d1c-9b6e-3f2a8d4c7e10" \ -X DELETE \ -H "Authorization: Bearer $OXUMI_API_KEY"