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, place a test call and watch the delivery log.
How a callback reaches you
- You start a call with
POST /v1/callsand setstatus_callback(and optionallystatus_method). - Each time the call changes state, Edesy sends your URL a request with the call's details.
- Your server returns a 2xx. Anything else, or a timeout, schedules a retry.
- 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
{
"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.
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+CallStatusand skip anything you've already processed. - Don't assume order. A retried
ringingcan arrive aftercompleted. 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
- Open the call's timeline with
GET /v1/calls/:sid/eventsto confirm the state changes happened. - Check the delivery log for the HTTP status and response your server returned.
- Fix the endpoint and replay the delivery.
The Voice API docs cover callbacks and signature checks in detail. For the wider product, see the Voice API hub, Voice API pricing (Rs 0.50/min, 60-second pulse, unanswered calls free, excluding 18% GST) and call routing for inbound numbers.