Short answer: an IVR outbound call API lets your software place a phone call that runs a pre-built call flow. You send one request with the phone number, the flow and some per-call data (name, order ID, amount). The flow speaks that data, waits for the customer to press a key, and calls your API with the choice while the call is still live. When the call ends, a signed callback tells your system what happened. That is the whole loop: request, flow, key press, webhook.
This post explains how that works on Edesy, using paths from the API reference. The same shape powers COD confirmation, EMI reminders and appointment confirmations.
The Pieces
| Piece | What it is |
|---|---|
| Flow | A call script built in the visual IVR builder: Say, Play, Menu, HTTP Request, Transfer, Voicemail, Email, Hang up. Must be published. |
| API key | Created in the IVR portal; sent as X-API-Key (or Authorization: Bearer). |
| Place a call | POST /v1/ivr/calls with the number, flow and variables. |
| HTTP Request node | Calls your API during the call, usually one per key. |
| Status callback | Your URL, called when the call ends. |
| Read back | GET /v1/ivr/calls/{call_sid} and GET /v1/ivr/call-logs. |
Build and test the flow first. The API only triggers it.
Placing a Call
POST /v1/ivr/calls
X-API-Key: <your key>
Idempotency-Key: order-A123-confirm
Content-Type: application/json
{
"flow_id": "<published flow id>",
"to": "9876543210",
"variables": { "name": "Ravi", "order_id": "A123", "amount_words": "four hundred ninety nine" },
"reference_id": "A123",
"status_callback_url": "https://your-app.example.com/ivr/status",
"ring_timeout_sec": 45
}tois an Indian number in any common form:9876543210,919876543210or+919876543210.fromis optional; it defaults to your first number assigned to IVR. It must be one of your own numbers.variablesare strings, numbers or booleans, used in the flow as{{var.name}}.reference_idis your own ID (up to 128 characters). It comes back in every callback, so you never have to map Edesy's call ID to your order.ring_timeout_secis 10 to 60 seconds, default 45.
The response is 202 Accepted with a call_sid and status queued. Accepted means queued for
dialling, not answered; the outcome comes later in the callback.
Idempotency: send it every time
Networks fail between your request and the response. Without protection, your retry places a
second call to the same customer. Send Idempotency-Key with a value unique to the business event
(order-A123-confirm, loan-881-due-oct):
- A repeat with the same key and body within 24 hours returns the original response and does not dial again.
- The same key with a different body returns
422 idempotency_key_reused. - A repeat while the first is still processing returns
409 idempotency_key_in_use.
Using Variables in the Flow
In Say text and Menu prompts, only {{var.<name>}} tokens are allowed:
Hello {{var.name}}, your order {{var.order_id}} is ready. To confirm, press 1.
A variable the call does not have is spoken as nothing, so test with real-looking data. For Hindi prompts, pass amounts and dates as words, because digits inside a Hindi sentence are not read reliably by every voice; see the Hindi IVR guide.
In HTTP Request URLs, headers and bodies, and in Email nodes, more tokens are available:
{{reference_id}}, {{call_sid}}, {{flow_id}}, {{caller}}, {{did}}, {{digits}} (the key
just pressed), {{menu_choice}} (that key's label) and {{time}} / {{time_iso}}.
Limits: 20 variables per call, names like order_id (letters, digits, underscore), values up to 512
characters, 4 KB in total.
Calling Your API on a Key Press
Put an HTTP Request node on each Menu branch. For key 1:
POST https://your-app.example.com/orders/{{reference_id}}/confirm
{ "order_id": "{{var.order_id}}", "choice": "{{digits}}", "call": "{{call_sid}}" }Two modes, and choosing the right one matters:
| Background (default) | Wait for response | |
|---|---|---|
| Caller experience | Call continues immediately | Caller waits 0.5 to 5 seconds |
| Retries | Yes, up to 5 attempts (default 3), backing off from 30 seconds | Never: the caller is waiting |
| Branching | One path onward | Success or Failure branch |
| Use for | Recording an answer, creating a task | When the next sentence depends on your answer |
Use background mode by default. Use wait-for-response only when the caller must hear a different message based on your result, for example "Cancelled" versus "Please try later". The flow can branch on success or failure of the request, not on values inside your response.
Token values are escaped for where they land: a variable cannot break out of a JSON string, add a
query parameter or header, or change the host. Tokens are never allowed in the host part of the URL,
only https:// URLs are accepted, redirects are not followed, and private or internal addresses
are refused.
Every request is listed under HTTP Logs in the portal, where a failed background request can be retried by hand.
The Status Callback
When the call ends, your status_callback_url gets a POST:
{
"event": "ivr.call.completed",
"data": {
"call_sid": "...", "reference_id": "A123", "flow_id": "...", "flow_version": 3,
"status": "completed", "hangup_cause": "normal",
"duration_sec": 42, "price": 1.5, "currency": "INR",
"dtmf_path": ["1"],
"variables": { "order_id": "A123" },
"http_requests": [ { "node_id": "http-1", "status": "delivered", "response_code": 200 } ]
}
}event is one of ivr.call.completed, ivr.call.no-answer, ivr.call.busy or ivr.call.failed.
dtmf_path is every key pressed, in order. http_requests tells you whether the in-call requests
reached you, which is useful when your endpoint was down. The callback is retried up to 5 times.
Treat the callback as the source of truth for retries: retry no-answer and busy on your own
schedule; do not retry completed.
Verifying That It Came From Edesy
Every HTTP Request and callback carries X-Edesy-Signature (sha256=<hex>) and X-Edesy-Timestamp.
Your signing secret is under API Keys, Developer in the portal, and can be rotated there.
Compute HMAC-SHA256(secret, timestamp + "." + rawBody), compare it with the header in constant
time, and reject timestamps older than five minutes:
import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
ts = headers["X-Edesy-Timestamp"]
expected = "sha256=" + hmac.new(
secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return abs(time.time() - int(ts)) < 300 and hmac.compare_digest(
expected, headers.get("X-Edesy-Signature", "")
)Use the raw request body. If your framework parses JSON first and you re-serialise it, the bytes
change and the signature will not match. Also store X-Edesy-Idempotency-Key and ignore repeats,
since a background request can be retried after your server already processed it.
Errors You Will Actually See
| HTTP | Code | What to do |
|---|---|---|
| 400 | invalid_phone |
Clean the number before calling |
| 402 | insufficient_balance |
Top up the wallet; alert on low balance |
| 409 | flow_not_published |
Publish the flow in the builder |
| 422 | dnc_suppressed |
Number is on your do-not-call list; do not retry |
| 422 | outside_calling_hours |
Queue it for the next window |
| 429 | concurrency_limit |
Too many API calls in progress; back off and retry |
| 502 | carrier_error |
The carrier rejected the call; retry later |
Limits to Design Around
- Calling hours: API calls are dialled only between 09:00 and 21:00 IST. Queue anything outside.
- Concurrency: 10 API calls in progress per workspace by default; ask support to raise it.
Your sender should respect
429rather than hammer. - HTTP Request rate: 20 requests per second per workspace, rendered body up to 16 KB, response read up to 64 KB.
- Wait-for-response time: at most five such requests and four seconds in total between two caller interactions. Keep synchronous endpoints fast.
A Minimal End-to-End Example
Flow: Play greeting, then Menu. Key 1 goes to a background HTTP Request to /confirm
then "Confirmed". Key 2 goes to a wait-for-response HTTP Request to /cancel; on success say
"Cancelled", on failure say "Please try later". Timeout hangs up. The portal's Order confirmation
(API + HTTP) template builds exactly this.
Your app then needs three things: a call to POST /v1/ivr/calls when an order is created, two small
endpoints for confirm and cancel, and one callback endpoint that records the outcome and schedules
a retry for no-answers.
What This API Does Not Do
So you can plan around it: the IVR has no built-in AI agent (a flow can stream audio to your own bot if you have one), menus take keypad input rather than speech, text-to-speech covers Hindi and English, and flows cannot yet branch on values inside your API response, only on success or failure.
Get API Access
See the endpoints, templates and pricing on the IVR API page, or talk to us about your integration.