POST/v1/calls dials to from a number you own and runs one instruction source when the callee answers. There are five sources: text (speak a message), url (fetch VoiceML from your server), voiceml (inline VoiceML), stream_url (stream audio to your bot) and application_id (use a saved application). Send exactly one.
Five ways to drive the call#
| Source | Use it when | Minimal body |
|---|---|---|
text |
You only need to speak a message | {"from","to","text"} |
url |
Your server decides what happens, per call | {"from","to","url"} |
voiceml |
You know the steps up front and want no webhook | {"from","to","voiceml"} |
stream_url |
Your own voice bot (Pipecat or custom) handles the audio | {"from","to","stream_url"} |
application_id |
You reuse a saved routing preset | {"from","to","application_id"} |
If you send none of them, Edesy uses the voice URL configured on the from number. If that is not set either, the request fails with 400 VALIDATION_ERROR.
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | Required | Destination in E.164, for example +9198XXXXXXXX |
from |
string | Required | A number in your workspace, E.164. Used as the caller ID |
text |
string | one source | Up to 2,000 characters, spoken with the default voice after answer |
voice |
string | Optional | Voice ID for text |
language |
string | Optional | Language for text, for example en-IN or hi-IN |
url |
string (URL) | one source | Answer URL. Fetched when the callee answers; must return VoiceML JSON |
method |
string | Optional | GET or POST for the answer URL. Edesy currently requests outbound answer URLs with POST |
voiceml |
string | one source | A VoiceML document as a JSON string, for example "{\"instructions\":[...]}" |
stream_url |
string (URI) | one source | wss:// endpoint for a bidirectional media stream. See Media Streams |
stream_sample_rate |
integer | Optional | 16000 (default) or 8000. Must match your bot |
application_id |
string (UUID) | one source | A saved application. application_sid is accepted as an alias. See Applications |
status_callback |
string (URL) | Optional | Receives lifecycle events. See Status Callbacks |
status_method |
string | Optional | POST (default, JSON body) or GET (query parameters) |
timeout |
integer | Optional | Ring timeout in seconds, 5 to 600. Default 30 |
caller_name |
string | Optional | Display name, up to 50 characters |
machine_detection |
string | Optional | Enable or DetectMessageEnd. Accepted for Twilio compatibility. Answering-machine detection with a recorded verdict runs on campaigns today; see AMD, Fallback Caller ID and DNC |
custom_parameters |
object | Optional | Your metadata, up to 20 keys and 2 KB. Echoed back on webhooks and in the media-stream metadata frame. tag is accepted as an alias |
to may also be sent as an object, {"type": "phone", "number": "+9198XXXXXXXX"}, for code copied from other platforms. Unknown fields are not an error: they are ignored and listed in meta.ignored_fields so you can spot a typo.
1. Speak text#
curl -X POST https://voice-api.edesy.in/v1/calls \
-H "Authorization: Bearer $EDESY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+9180XXXXXXXX",
"to": "+9198XXXXXXXX",
"text": "नमस्ते, आपका ऑर्डर भेज दिया गया है।",
"language": "hi-IN"
}'
2. Answer URL (VoiceML webhook)#
curl -X POST https://voice-api.edesy.in/v1/calls \
-H "Authorization: Bearer $EDESY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+9180XXXXXXXX",
"to": "+9198XXXXXXXX",
"url": "https://example.com/edesy/answer",
"status_callback": "https://example.com/edesy/status",
"custom_parameters": { "customer_id": "C-881" }
}'
When the callee answers, Edesy sends a form-encoded POST to your url:
POST /edesy/answer
Content-Type: application/x-www-form-urlencoded
CallSid=CA_6f1c...&From=%2B9180XXXXXXXX&To=%2B9198XXXXXXXX&Direction=outbound-api&customer_id=C-881
The fields are CallSid, AccountSid, From, To, CallStatus and Direction, plus each of your custom_parameters as its own field. Reply with 200 and a VoiceML JSON body (up to 1 MB):
{
"instructions": [
{ "verb": "say", "text": "Hi, this is Acme. Your order is out for delivery." },
{ "verb": "play", "url": "https://cdn.example.com/jingle.mp3" },
{ "verb": "hangup" }
]
}
The answer URL must return VoiceML, not TwiML. See the VoiceML Reference. If your URL fails, times out, returns a non-2xx status or returns invalid VoiceML, the callee hears a short spoken error message and the call ends. Check the call's event timeline and test your endpoint before going live.
3. Inline VoiceML#
curl -X POST https://voice-api.edesy.in/v1/calls \
-H "Authorization: Bearer $EDESY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+9180XXXXXXXX",
"to": "+9198XXXXXXXX",
"voiceml": "{\"instructions\":[{\"verb\":\"say\",\"text\":\"Your appointment is tomorrow at 10 AM.\"},{\"verb\":\"pause\",\"length\":1},{\"verb\":\"say\",\"text\":\"Thank you.\"},{\"verb\":\"hangup\"}]}"
}'
voiceml is a string, so escape the inner quotes, or build it in code with JSON.stringify(doc) / json.dumps(doc).
4. Stream to your bot#
curl -X POST https://voice-api.edesy.in/v1/calls \
-H "Authorization: Bearer $EDESY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+9180XXXXXXXX",
"to": "+9198XXXXXXXX",
"stream_url": "wss://bot.example.com/ws",
"stream_sample_rate": 16000,
"custom_parameters": { "assistant_id": "asst_456" }
}'
When the callee answers, Edesy opens a WebSocket to your bot and streams raw L16 PCM both ways. Run POST/v1/stream-test first to check that your bot is reachable. See Media Streams.
5. Saved application#
curl -X POST https://voice-api.edesy.in/v1/calls \
-H "Authorization: Bearer $EDESY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+9180XXXXXXXX",
"to": "+9198XXXXXXXX",
"application_id": "8d5c1e2a-0000-4000-8000-000000000000"
}'
The application's webhook or media-stream target drives the call, and its status callback and recording settings apply. Only applications whose target is a webhook or a media stream can drive an outbound call. See Applications.
Response 201#
{
"data": {
"call_sid": "CA_6f1c2d3e-0000-4000-8000-000000000000",
"sid": "CA_6f1c2d3e-0000-4000-8000-000000000000",
"account_sid": "2b7e0a1c-0000-4000-8000-000000000000",
"from": "+9180XXXXXXXX",
"to": "+9198XXXXXXXX",
"direction": "outbound-api",
"status": "queued",
"custom_parameters": { "customer_id": "C-881" },
"created_at": "2026-10-28T09:30:00Z"
},
"meta": { "request_id": "req_...", "timestamp": "2026-10-28T09:30:00Z" }
}
Errors#
| HTTP | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
Missing to/from, more than one instruction source, no source at all, or a field out of range |
| 401 | UNAUTHORIZED |
Missing or invalid credentials |
| 402 | INSUFFICIENT_BALANCE |
Wallet too low to place the call |
| 403 | NUMBER_NOT_OWNED |
from is not a number in your workspace |
| 429 | CONCURRENCY_LIMIT / CPS_LIMIT |
All channels in use, or calls per second exceeded. Wait for Retry-After seconds |
| 503 | SERVICE_UNAVAILABLE |
No route available right now; retry shortly |
See Error Codes.