Hangup Webhook
The Hangup Webhook is called when a call ends, regardless of the reason (completed, failed, canceled, etc.). This webhook provides call detail records (CDR) including duration, timestamps, and call status.
This webhook is fire-and-forget — Superfone does not wait for your response or retry on failure. The response body is ignored. Use this webhook to log call data, update databases, or trigger post-call workflows.
When It's Called
The hangup webhook is triggered when:
- Call completes normally (both parties hang up)
- Callee doesn't answer (ring timeout)
- Callee is busy
- Call is canceled before being answered
- Call fails due to network issues or other errors
Webhook Payload
Superfone sends a POST or GET request (based on your app configuration) with the following payload:
Payload Structure
| Field | Type | Description |
|---|---|---|
call_uuid | string | Unique identifier for this call |
request_uuid | string | Request UUID for outbound calls (format: sfv_ob_req_xxxxxxxxxxxx). Not present for inbound calls. |
from | string | Caller's phone number (E.164 format, e.g., +918000000003) |
to | string | Callee's phone number (E.164 format, e.g., +918000000001) |
direction | string | Call direction: INBOUND or OUTBOUND |
call_status | string | Final call status: COMPLETED, NO_ANSWER, BUSY, CANCELED, or FAILED |
duration | number | Call duration in seconds (time from answer to hangup). 0 if call was not answered. |
started_at | string | ISO 8601 timestamp when call started ringing (e.g., 2026-02-02T10:00:00.000Z) |
answer_at | string | ISO 8601 timestamp when call was answered. undefined if call was not answered. |
ended_at | string | ISO 8601 timestamp when call ended (e.g., 2026-02-02T10:05:00.000Z) |
hangup_cause | string | Hangup cause code (e.g., 16 for normal clearing). Optional. |
Call Status Values
The call_status field indicates why the call ended:
| Status | Description | When It Occurs |
|---|---|---|
COMPLETED | Call answered and hung up normally | Both parties connected, call ended normally |
NO_ANSWER | Callee didn't answer | Ring timeout reached, callee didn't pick up |
BUSY | Callee was busy | Callee's line was busy |
CANCELED | Call canceled before being answered | Caller hung up before callee answered |
FAILED | Call failed | Network issue, invalid number, unreachable, or other error |
Superfone maps call outcomes to these statuses:
ANSWER,BACKUP_ANSWER,IVR_ANSWERED→COMPLETEDNOANSWER,BACKUP_NOANSWER→NO_ANSWERBUSY,BACKUP_BUSY→BUSYCANCEL,BACKUP_CANCEL→CANCELEDCONGESTION,CHANUNAVAIL,REJECTED,BLOCKED→FAILED
Example Payloads
- Completed Call
- No Answer
- Busy
- Canceled
- Failed
{
"call_uuid": "call-uuid-1738491600-abc123",
"request_uuid": "sfv_ob_req_a8k3m2x9p1z0",
"from": "+918000000003",
"to": "+918000000001",
"direction": "OUTBOUND",
"call_status": "COMPLETED",
"duration": 120,
"started_at": "2026-02-02T10:00:00.000Z",
"answer_at": "2026-02-02T10:00:10.000Z",
"ended_at": "2026-02-02T10:02:10.000Z",
"hangup_cause": "16"
}
Notes:
duration: 120 seconds (2 minutes)answer_at: Call was answered 10 seconds after ringing startedhangup_cause:16= Normal call clearing
{
"call_uuid": "call-uuid-1738491600-def456",
"request_uuid": "sfv_ob_req_b9l4n3y0q2a1",
"from": "+918000000003",
"to": "+918000000001",
"direction": "OUTBOUND",
"call_status": "NO_ANSWER",
"duration": 0,
"started_at": "2026-02-02T10:00:00.000Z",
"ended_at": "2026-02-02T10:01:00.000Z",
"hangup_cause": "19"
}
Notes:
duration: 0 (call was never answered)answer_at: Not present (call was not answered)hangup_cause:19= No answer
{
"call_uuid": "call-uuid-1738491600-ghi789",
"request_uuid": "sfv_ob_req_c0m5o4z1r3b2",
"from": "+918000000003",
"to": "+918000000001",
"direction": "OUTBOUND",
"call_status": "BUSY",
"duration": 0,
"started_at": "2026-02-02T10:00:00.000Z",
"ended_at": "2026-02-02T10:00:05.000Z",
"hangup_cause": "17"
}
Notes:
duration: 0 (call was never answered)answer_at: Not present (call was not answered)hangup_cause:17= User busy
{
"call_uuid": "call-uuid-1738491600-jkl012",
"request_uuid": "sfv_ob_req_d1n6p5a2s4c3",
"from": "+918000000003",
"to": "+918000000001",
"direction": "OUTBOUND",
"call_status": "CANCELED",
"duration": 0,
"started_at": "2026-02-02T10:00:00.000Z",
"ended_at": "2026-02-02T10:00:15.000Z",
"hangup_cause": "487"
}
Notes:
duration: 0 (call was never answered)answer_at: Not present (call was not answered)hangup_cause:487= Request terminated
{
"call_uuid": "call-uuid-1738491600-mno345",
"request_uuid": "sfv_ob_req_e2o7q6b3t5d4",
"from": "+918000000003",
"to": "+918000000001",
"direction": "OUTBOUND",
"call_status": "FAILED",
"duration": 0,
"started_at": "2026-02-02T10:00:00.000Z",
"ended_at": "2026-02-02T10:00:02.000Z",
"hangup_cause": "503"
}
Notes:
duration: 0 (call was never answered)answer_at: Not present (call was not answered)hangup_cause:503= Service unavailable
Response Handling
Your webhook response is ignored by Superfone. You can return any HTTP 2xx status to acknowledge receipt:
{
"received": true
}
Return a response quickly (ideally <100ms). Perform long-running tasks (database writes, API calls) asynchronously after responding.
Webhook Handler Examples
- JavaScript
- TypeScript
- Python
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/hangup', (req, res) => {
const payload = req.body;
console.log('[Hangup Webhook] Call ended:', {
call_uuid: payload.call_uuid,
request_uuid: payload.request_uuid,
from: payload.from,
to: payload.to,
direction: payload.direction,
call_status: payload.call_status,
duration: payload.duration,
started_at: payload.started_at,
answer_at: payload.answer_at,
ended_at: payload.ended_at,
hangup_cause: payload.hangup_cause
});
// Respond immediately
res.json({ received: true });
// Process asynchronously (don't block response)
processCallData(payload).catch(err => {
console.error('Error processing call data:', err);
});
});
async function processCallData(payload) {
// Save to database
await db.calls.insert({
call_uuid: payload.call_uuid,
from: payload.from,
to: payload.to,
status: payload.call_status,
duration: payload.duration,
started_at: new Date(payload.started_at),
ended_at: new Date(payload.ended_at)
});
// Trigger post-call workflow
if (payload.call_status === 'COMPLETED' && payload.duration > 60) {
await sendFollowUpEmail(payload.from, payload.to);
}
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
import express, { Request, Response } from 'express';
interface HangupWebhookPayload {
call_uuid: string;
request_uuid?: string;
from: string;
to: string;
direction: 'INBOUND' | 'OUTBOUND';
call_status: 'COMPLETED' | 'NO_ANSWER' | 'BUSY' | 'CANCELED' | 'FAILED';
duration: number;
started_at: string;
answer_at?: string;
ended_at: string;
hangup_cause?: string;
}
const app = express();
app.use(express.json());
app.post('/webhook/hangup', (req: Request, res: Response) => {
const payload = req.body as HangupWebhookPayload;
console.log('[Hangup Webhook] Call ended:', {
call_uuid: payload.call_uuid,
request_uuid: payload.request_uuid,
from: payload.from,
to: payload.to,
direction: payload.direction,
call_status: payload.call_status,
duration: payload.duration,
started_at: payload.started_at,
answer_at: payload.answer_at,
ended_at: payload.ended_at,
hangup_cause: payload.hangup_cause
});
// Respond immediately
res.json({ received: true });
// Process asynchronously (don't block response)
processCallData(payload).catch(err => {
console.error('Error processing call data:', err);
});
});
async function processCallData(payload: HangupWebhookPayload): Promise<void> {
// Save to database
await db.calls.insert({
call_uuid: payload.call_uuid,
from: payload.from,
to: payload.to,
status: payload.call_status,
duration: payload.duration,
started_at: new Date(payload.started_at),
ended_at: new Date(payload.ended_at)
});
// Trigger post-call workflow
if (payload.call_status === 'COMPLETED' && payload.duration > 60) {
await sendFollowUpEmail(payload.from, payload.to);
}
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
from flask import Flask, request, jsonify
import asyncio
from datetime import datetime
app = Flask(__name__)
@app.route('/webhook/hangup', methods=['POST'])
def hangup_webhook():
payload = request.json
print('[Hangup Webhook] Call ended:', {
'call_uuid': payload['call_uuid'],
'request_uuid': payload.get('request_uuid', 'N/A'),
'from': payload['from'],
'to': payload['to'],
'direction': payload['direction'],
'call_status': payload['call_status'],
'duration': payload['duration'],
'started_at': payload['started_at'],
'answer_at': payload.get('answer_at', 'N/A'),
'ended_at': payload['ended_at'],
'hangup_cause': payload.get('hangup_cause', 'N/A')
})
# Respond immediately
response = jsonify({'received': True})
# Process asynchronously (don't block response)
asyncio.create_task(process_call_data(payload))
return response
async def process_call_data(payload):
try:
# Save to database
await db.calls.insert({
'call_uuid': payload['call_uuid'],
'from': payload['from'],
'to': payload['to'],
'status': payload['call_status'],
'duration': payload['duration'],
'started_at': datetime.fromisoformat(payload['started_at']),
'ended_at': datetime.fromisoformat(payload['ended_at'])
})
# Trigger post-call workflow
if payload['call_status'] == 'COMPLETED' and payload['duration'] > 60:
await send_follow_up_email(payload['from'], payload['to'])
except Exception as e:
print(f'Error processing call data: {e}')
if __name__ == '__main__':
app.run(port=3000)
Timeout
Superfone waits 5 seconds for your webhook to respond. If your server doesn't respond within this time, the request is considered failed and logged, but this does not affect call processing (fire-and-forget).
Do not perform long-running operations (database writes, external API calls) before responding. Respond immediately and process data asynchronously.
Use Cases
1. Call Analytics
Track call metrics and generate reports:
app.post('/webhook/hangup', async (req, res) => {
const { call_status, duration, direction } = req.body;
// Respond immediately
res.json({ received: true });
// Update analytics asynchronously
await analytics.track({
event: 'call_ended',
properties: {
status: call_status,
duration,
direction,
timestamp: new Date()
}
});
});
2. CRM Integration
Update customer records with call data:
app.post('/webhook/hangup', async (req, res) => {
const { from, to, call_status, duration } = req.body;
// Respond immediately
res.json({ received: true });
// Update CRM asynchronously
if (call_status === 'COMPLETED') {
await crm.updateContact(to, {
last_call_date: new Date(),
last_call_duration: duration,
last_call_status: call_status
});
}
});
3. Missed Call Notifications
Send notifications for missed calls:
app.post('/webhook/hangup', async (req, res) => {
const { call_status, from, to } = req.body;
// Respond immediately
res.json({ received: true });
// Send notification asynchronously
if (call_status === 'NO_ANSWER') {
await sendSMS(to, `You missed a call from ${from}`);
}
});
4. Billing and Usage Tracking
Calculate call costs and update billing:
app.post('/webhook/hangup', async (req, res) => {
const { call_uuid, duration, direction } = req.body;
// Respond immediately
res.json({ received: true });
// Calculate cost asynchronously
const costPerMinute = direction === 'OUTBOUND' ? 0.05 : 0.02;
const cost = (duration / 60) * costPerMinute;
await billing.recordCharge({
call_uuid,
duration,
cost,
timestamp: new Date()
});
});
Notes
- Fire-and-Forget: Response is ignored — return quickly and process asynchronously
- No Retries: Superfone does not retry failed hangup webhooks
- Idempotency: Use
call_uuidto avoid duplicate processing if webhook is called multiple times - Duration Calculation:
durationis time fromanswer_attoended_at. If call was not answered,durationis0. - Timestamp Format: All timestamps are ISO 8601 strings (e.g.,
2026-02-02T10:00:00.000Z) - Hangup Cause Codes: See the Common Hangup Cause Codes table below for
hangup_causemeanings
Common Hangup Cause Codes
| Code | Meaning | Typical Scenario |
|---|---|---|
16 | Normal call clearing | Call completed normally |
17 | User busy | Callee's line was busy |
19 | No answer | Ring timeout, callee didn't answer |
21 | Call rejected | Callee rejected the call |
487 | Request terminated | Caller canceled before answer |
503 | Service unavailable | Network issue, server unreachable |
Related Endpoints
- Answer Webhook — Handle call answered events
- Webhooks Overview — Learn about webhook types and configuration
- Create App API — Configure hangup webhook URL
- Initiate Call API — Override hangup webhook per call
Next Steps
- Track call metrics and generate reports
- Update customer records with call data
- Calculate call costs and update billing