# IVR Outbound Call API: Trigger a Call From Your App

> How an IVR outbound call API works: placing a call with per-call variables, idempotency, calling HTTP endpoints on key presses, status callbacks, HMAC signature verification, limits and error codes, with examples for India.

Source: https://edesy.in/blog/ivr-outbound-call-api-guide-2026

**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](https://edesy.in/blog/cod-confirmation-webhook-api-guide-2026),
[EMI reminders](https://edesy.in/blog/emi-payment-reminder-ivr-calls-2026) and
[appointment confirmations](https://edesy.in/blog/appointment-confirmation-ivr-calls-2026).

## 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

```http
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
}
```

- **`to`** is an Indian number in any common form: `9876543210`, `919876543210` or `+919876543210`.
- **`from`** is optional; it defaults to your first number assigned to IVR. It must be one of your
  own numbers.
- **`variables`** are strings, numbers or booleans, used in the flow as `{{var.name}}`.
- **`reference_id`** is 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_sec`** is 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](https://edesy.in/blog/hindi-ivr-setup-guide-2026).

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:

```json
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`:

```json
{
  "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:

```python
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 `429` rather 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](https://edesy.in/ivr/api), or
[talk to us](https://edesy.in/contact) about your integration.

## Related Reading

- [What is DTMF? Keypresses and IVR](https://edesy.in/blog/what-is-dtmf-ivr-keypress-guide-2026)
- [Webhooks that silently stopped: delivery health](https://edesy.in/blog/webhook-silently-stopped-delivery-health-2026)
- [Trigger payment reminder calls from your app](https://edesy.in/blog/trigger-payment-reminder-calls-from-your-app-api-2026)
- [EMI and payment reminder IVR calls](https://edesy.in/blog/emi-payment-reminder-ivr-calls-2026)

## Frequently Asked Questions

### Do I need to host anything to receive key presses?

Only an HTTPS endpoint, if you want key presses or call results sent to your system. The call itself, the audio, the keypad handling and retries run on Edesy. Without an endpoint, you can still read results with GET requests on the call or call-log endpoints.

### Can the call speak data from my system, like an order number or amount?

Yes. Pass up to 20 variables in the request and reference them in Say text and Menu prompts as {{var.name}}. Text-to-speech is available in Hindi and English. For other languages, use recorded audio prompts, which cannot include per-call data.

### Is there a sandbox or test mode?

Use the builder's test call to check a flow on your own phone, and place your first API calls to your own number. Calls are charged from your prepaid wallet like any other call, and test calls are short.

### Can an AI agent take over the call?

Edesy IVR does not include a built-in AI agent. A flow can stream the call's audio to your own WebSocket bot if you run one; otherwise use keypad menus, transfers and HTTP requests.
