# Call Status Callbacks Explained: Statuses, Hangup Reasons and Retries (2026)

> What a voice API status callback is, which statuses a call goes through, how to read SIP codes, hangup reasons and call outcomes, and how to verify, deduplicate and replay callbacks.

Source: https://edesy.in/blog/call-status-callbacks-explained-2026

**A call status callback is an HTTP request your voice provider sends each time a call changes state: initiated,
ringing, in-progress, then a final status such as completed, busy, no-answer or failed. On Edesy each callback
uses Twilio-style fields, adds the SIP code, a plain hangup reason and a call outcome, is HMAC-signed, and is
retried with backoff if your server doesn't answer.**

> **See callbacks on a real call:** [Open the Edesy voice portal](https://voice-app.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=call-status-callbacks-explained-2026), place a test call and watch the delivery log.

## How a callback reaches you

1. You start a call with `POST /v1/calls` and set `status_callback` (and optionally `status_method`).
2. Each time the call changes state, Edesy sends your URL a request with the call's details.
3. Your server returns a 2xx. Anything else, or a timeout, schedules a retry.
4. Every attempt shows in the debugger's webhook deliveries, where you can replay it.

POST callbacks carry a JSON body. GET callbacks carry the same fields as query parameters.

## The payload

```json
{
  "CallSid": "CA...",
  "AccountSid": "...",
  "From": "+9180XXXXXXXX",
  "To": "+9198XXXXXXXX",
  "CallStatus": "no-answer",
  "Direction": "outbound",
  "Duration": 0,
  "SipResponseCode": 480,
  "HangupReason": "unavailable",
  "CallOutcome": "unreachable",
  "Timestamp": "2026-10-28T09:30:00Z"
}
```

`SipResponseCode`, `HangupReason` and `CallOutcome` appear on final events. Any `custom_parameters` you sent when
creating the call are echoed back, so you can tie a callback to your own order or lead ID without a lookup.

## Call statuses

| CallStatus | Meaning | Final? |
|---|---|---|
| `initiated` | The call request was accepted | No |
| `ringing` | The other phone is ringing | No |
| `in-progress` | The call was answered | No |
| `completed` | An answered call ended normally | Yes |
| `busy` | Engaged or rejected | Yes |
| `no-answer` | Rang out, or the phone was off or out of coverage | Yes |
| `canceled` | The caller side hung up before answer | Yes |
| `failed` | The call didn't reach the person | Yes |

On the final event, Edesy derives `CallStatus` from the SIP answer rather than only the carrier's coarse status.
A SIP 480 (phone switched off) is reported as `no-answer`, not `failed`, which matters for retry logic.

## Hangup reasons

| SIP code | CallStatus | HangupReason |
|---|---|---|
| 200 | completed | completed |
| 486, 600 | busy | busy |
| 603 | busy | rejected |
| 487, or 408 after ringing | no-answer | no_answer |
| 487 when the caller hung up first | canceled | caller_canceled |
| 480 | no-answer | unavailable |
| 404, 410, 484, 485, 604 | failed | invalid_number |
| 401, 403, 407 | failed | call_blocked |
| 408 before ringing | failed | network_timeout |
| 402, 429, 5xx | failed | carrier_error |
| none or other | failed | call_failed |

## Call outcome: the field to build retries on

`CallOutcome` groups the reason by who or what the call ended at.

| CallOutcome | What it means | What to do |
|---|---|---|
| `answered` | The person answered | Done |
| `callee` | Busy, rejected, no answer or canceled | Retry later, at a different time |
| `unreachable` | Phone off or out of coverage | Retry after a gap |
| `invalid_number` | The number doesn't exist | Don't retry; fix the data |
| `network` | The telecom network didn't connect the call | Usually temporary; retry soon |
| `account` | Refused by your balance or number settings | Top up or fix settings |
| `platform` | Edesy couldn't place the call | Contact support |

This is more useful than branching on `failed`, because `failed` covers both "bad number, stop" and "network blip,
try again".

## Verify the signature

Each delivery has `X-Webhook-Timestamp` and `X-Webhook-Signature: v1=<hex>`. The signature is HMAC-SHA256 of
`<timestamp>.<raw body>` with your signing secret.

```js
import crypto from "node:crypto";

function verify(rawBody, timestamp, signatureHeader, secret) {
  const expected = "v1=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

Use the raw request body, not a re-serialised object, or the bytes won't match. Rejecting requests with an old
timestamp (a few minutes is a common window) guards against replays by a third party.

## Retries, duplicates and order

Edesy retries failed deliveries at 30 seconds, 1 minute, 2, 4 and 8 minutes, for 6 attempts in total. Because of
retries:

- **Return 2xx fast.** Queue the work and respond; don't call other services first.
- **Be idempotent.** Store `CallSid` + `CallStatus` and skip anything you've already processed.
- **Don't assume order.** A retried `ringing` can arrive after `completed`. Never let a non-final status overwrite
  a final one.

## Webhook subscriptions: account-wide events

Per-call `status_callback` is set on each request. If you'd rather receive every call's events at one endpoint,
use webhook subscriptions (`/v1/webhooks/subscriptions`). Events include `call.initiated`, `call.ringing`,
`call.answered`, `call.completed`, `call.failed`, `call.missed`, `recording.ready` and `disposition.submitted`.
Subscriptions support a test send, a delivery list and replay.

| | Per-call `status_callback` | Webhook subscription |
|---|---|---|
| Set where | On each `POST /v1/calls` | Once per account |
| Good for | Flow-specific handling | Central logging, CRM sync |
| Payload | Twilio-style PascalCase | Event-based |
| Signed and retried | Yes | Yes |

## Debugging a missing callback

1. Open the call's timeline with `GET /v1/calls/:sid/events` to confirm the state changes happened.
2. Check the delivery log for the HTTP status and response your server returned.
3. Fix the endpoint and replay the delivery.

The [Voice API docs](https://edesy.in/docs/voice-api) cover callbacks and signature checks in detail. For the wider product, see
the [Voice API hub](https://edesy.in/voice-api), [Voice API pricing](https://edesy.in/voice-api/pricing) (Rs 0.50/min, 60-second pulse,
unanswered calls free, excluding 18% GST) and [call routing](https://edesy.in/call-routing) for inbound numbers.

## Frequently Asked Questions

### What is a call status callback?

It is an HTTP request the voice platform sends to your server each time a call changes state, for example ringing, answered or completed. It lets your app update records and decide on retries without polling the API.

### What is the difference between busy, no-answer and failed?

Busy means the person's phone rejected or was engaged. No-answer means it rang (or the phone was switched off, SIP 480) and nobody picked up. Failed means the call never reached the person, for example an invalid number, a blocked call or a network error.

### Why should I verify the callback signature?

Anyone can POST to a public URL. Edesy signs each delivery with HMAC-SHA256 using your signing secret, so checking X-Webhook-Signature against the timestamp and raw body proves the request came from Edesy and wasn't changed.

### What happens if my server is down when a callback is sent?

Edesy retries with backoff at 30 seconds, 1, 2, 4 and 8 minutes, for 6 attempts in total. Every attempt is in the delivery log, and you can replay a delivery from the portal or API.

### Can the same callback arrive twice?

Yes, any retried webhook can. Make your handler idempotent: key your updates on CallSid plus CallStatus and ignore a status you have already recorded.

### Does a slow callback endpoint affect the call?

No. Delivery is separate from the call itself, so a slow or failing status endpoint never delays or drops the call. You just get the update later.
