# Outbound IVR Calls

> POST /v1/ivr/calls - call an Indian number and run a published IVR flow with per-call variables, a reference ID, a signed status callback and an idempotency key.

Source: https://edesy.in/docs/ivr/api/outbound-ivr-calls

`POST /v1/ivr/calls` calls an Indian phone number and runs one of your published IVR flows on the call. Pass per-call `variables` (used as `{{var.name}}`), your own `reference_id`, and a `status_callback_url` that receives the signed result when the call ends. An `Idempotency-Key` header makes retries safe. DNC and 09:00-21:00 IST calling hours are enforced.

`POST https://voice-api.edesy.in/v1/ivr/calls` → **202 Accepted**

## Request body

| Field | Type | Required | Notes |
|---|---|---|---|
| `flow_id` | string (UUID) | yes | The flow must be **published** |
| `to` | string | yes | Indian number: `9800000000`, `919800000000` or `+919800000000` |
| `from` | string | no | One of your numbers. Default: your first number assigned to IVR |
| `variables` | object | no | Strings, numbers or booleans. Used as `{{var.<name>}}`. See [limits](#limits) |
| `reference_id` | string | no | Your own ID, up to 128 characters. Echoed in callbacks and available as `{{reference_id}}` |
| `status_callback_url` | string | no | `https://` only, up to 2048 characters. Receives the call result |
| `ring_timeout_sec` | integer | no | 10 to 60. Default 45 |

## Headers

| Header | Notes |
|---|---|
| `Authorization: Bearer sk_...` or `X-API-Key: sk_...` | Required |
| `Idempotency-Key` | Optional, up to 128 characters. See [idempotency](#idempotency) |
| `Content-Type: application/json` | Required |

## Example

```bash
curl -X POST https://voice-api.edesy.in/v1/ivr/calls \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Idempotency-Key: order-A123-confirm" \
  -H "Content-Type: application/json" \
  -d '{
    "flow_id": "5b1c2f0e-0000-4000-8000-000000000000",
    "to": "+919800000000",
    "variables": { "order_id": "A123", "name": "Ravi", "amount": 499 },
    "reference_id": "crm-lead-42",
    "status_callback_url": "https://crm.example.com/ivr/status"
  }'
```

<!-- response 202 -->
```json
{
  "data": {
    "call_sid": "a1b2c3d4-0000-4000-8000-000000000000",
    "call_log_id": "7f3e2d1c-0000-4000-8000-000000000000",
    "status": "queued",
    "reference_id": "crm-lead-42"
  }
}
```

## How it works

1. Edesy checks the flow is published, the number is valid and not on your do-not-call list, the time is within calling hours, your wallet has balance, and you are under the concurrency limit.
2. The call is queued and dialled from `from` (or your default IVR number).
3. When the person answers, the flow runs with your `variables` filled in.
4. HTTP Request nodes in the flow call your API as the caller presses keys.
5. When the call ends, Edesy POSTs the result to `status_callback_url`.

## Idempotency

Send `Idempotency-Key: <unique string>` to make retries safe:

- 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 request is still being processed returns `409 idempotency_key_in_use`.

## Errors

| HTTP | `code` | Meaning |
|---|---|---|
| 400 | `invalid_request` | Body is not valid JSON |
| 400 | `validation_error` | Bad field, variables, callback URL or Idempotency-Key |
| 400 | `invalid_phone` | `to` is not a valid Indian number |
| 400 | `no_caller_id` | No `from` and no number assigned to IVR |
| 402 | `insufficient_balance` | Top up your wallet |
| 403 | `from_not_owned` | `from` is not one of your numbers |
| 404 | `flow_not_found` | No such flow in this workspace |
| 409 | `flow_not_published` | Publish the flow first |
| 409 | `idempotency_key_in_use` | The same key is still being processed |
| 422 | `idempotency_key_reused` | Same key, different body |
| 422 | `dnc_suppressed` | The number is on your do-not-call list |
| 422 | `outside_calling_hours` | Outside 09:00-21:00 IST |
| 429 | `concurrency_limit` | Too many of your API calls are in progress |
| 502 | `carrier_error` | The carrier rejected the call |

## Get a call

`GET https://voice-api.edesy.in/v1/ivr/calls/{call_sid}` → **200 OK**

Returns the call log for any IVR call (API, inbound, test or order automation): status, duration, price, `dtmf_path`, `variables`, `reference_id`, `call_outcome`, and `http_requests` (every HTTP request the flow made during the call). Secret-looking header values are masked. Unknown calls return `404 not_found`.

To list API calls, use `GET /v1/ivr/call-logs?source=api&reference_id=crm-lead-42`. See [Logs API](https://edesy.in/docs/ivr/api/logs).

## Status callback

When the call ends, Edesy sends `POST` to your `status_callback_url`. It is [signed](https://edesy.in/docs/ivr/api/http-signing-secret) like HTTP Request nodes and makes up to 5 attempts, with backoff, if your endpoint fails.

<!-- code-title Status callback payload -->
```json
{
  "event": "ivr.call.completed",
  "timestamp": "2026-10-28T10:15:00Z",
  "data": {
    "call_sid": "a1b2c3d4-0000-4000-8000-000000000000",
    "call_log_id": "7f3e2d1c-0000-4000-8000-000000000000",
    "reference_id": "crm-lead-42",
    "flow_id": "5b1c2f0e-0000-4000-8000-000000000000",
    "flow_version": 3,
    "from": "918000000000",
    "to": "919800000000",
    "status": "completed",
    "hangup_cause": "normal",
    "sip_status": 0,
    "call_outcome": "answered",
    "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 } ],
    "answers": [ { "question_key": "city", "question": "Which city are you calling from?", "transcript": "Lucknow", "status": "captured", "attempt": 1 } ]
  }
}
```

| Field | Values |
|---|---|
| `event` | `ivr.call.completed`, `ivr.call.no-answer`, `ivr.call.busy`, `ivr.call.canceled`, `ivr.call.failed` |
| `hangup_cause` | `normal` for an answered call. Otherwise the reason: `busy`, `rejected`, `no_answer`, `unavailable`, `invalid_number`, `call_blocked`, `network_timeout`, `carrier_error`, `caller_canceled`, `call_failed` |
| `sip_status` | The SIP response code for a call that did not connect. A 480 (switched off) is reported as no-answer, a 603 as busy |
| `call_outcome` | `answered`, `callee`, `unreachable`, `invalid_number`, `network` (usually temporary; retry later), `account` or `platform` (Edesy could not place it) |
| `answers` | Spoken answers from [Ask](https://edesy.in/docs/ivr/nodes/ask) nodes |

Status callbacks are listed in [HTTP Logs](https://edesy.in/docs/ivr/logs/http-logs) with the kind **Status callback**.

## Limits

| Limit | Value |
|---|---|
| Variables per call | 20 keys. Name `[a-zA-Z_][a-zA-Z0-9_]{0,63}`, value up to 512 characters, 4 KB in total |
| Concurrent API calls | 10 per workspace by default. Ask support to raise it |
| Calling hours | 09:00-21:00 IST |
| Do-not-call list | Numbers on your DNC list are refused with `dnc_suppressed` |
| Ring timeout | 10 to 60 seconds, default 45 |

## Pricing

Rs 1.50 per minute, billed per started minute from your wallet. Unanswered calls are free. Prices exclude 18% GST. See [Pricing](https://edesy.in/docs/ivr/getting-started/pricing).
