Skip to main content

Incoming Call Webhook

/api/v1/call/incoming
POST
Handles incoming call events from Telnyx telephony service

Endpoint

POST /api/v1/call/incoming
POST /api/v1/call/incoming/
Both routes (with and without trailing slash) are supported.

Description

This webhook endpoint receives call lifecycle events from Telnyx including call initiation, answering, and hangup. The system processes these events to manage call state, trigger audio streaming, perform real-time analysis, and create dispatch queue items.

Request

Headers

Content-Type
string
default:"application/json"
required
Must be application/json

Request Body

Telnyx sends events with the following structure:
data
object
required
Container for event data
data.event_type
string
required
Type of call event. Values: call.initiated, call.answered, call.hangup
data.payload
object
required
Event-specific payload with call details
data.payload.call_session_id
string
required
Unique identifier for the call
data.payload.call_control_id
string
required
Control ID used for call actions (answer, stream, hangup)
data.payload.from
string
Caller’s phone number
data.payload.to
string
Destination phone number
data.payload.start_time
string
ISO 8601 timestamp when call started
data.payload.end_time
string
ISO 8601 timestamp when call ended (only in call.hangup)

Example: Call Initiated

{
  "data": {
    "event_type": "call.initiated",
    "payload": {
      "call_session_id": "abc123-def456-ghi789",
      "call_control_id": "v3:xyz789-abc123",
      "from": "+15555551234",
      "to": "+15555559911",
      "start_time": "2026-03-03T23:15:30.000Z"
    }
  }
}

Example: Call Hangup

{
  "data": {
    "event_type": "call.hangup",
    "payload": {
      "call_session_id": "abc123-def456-ghi789",
      "call_control_id": "v3:xyz789-abc123",
      "from": "+15555551234",
      "to": "+15555559911",
      "start_time": "2026-03-03T23:15:30.000Z",
      "end_time": "2026-03-03T23:17:45.000Z"
    }
  }
}

Response

status
string
Always returns "ok" for successful webhook processing

Success Response (200 OK)

{
  "status": "ok"
}

Event Processing

call.initiated

When a call is initiated:
  1. Maps call_control_id to call_session_id for WebSocket routing
  2. Creates entry in LIVE_CALLS dictionary with initial state
  3. Triggers background task to answer call and start audio streaming
  4. Sends TTS greeting: “This is Dispatch AI. I’m listening. Please describe your emergency.”

call.answered

When a call is answered:
  1. Updates call status to ACTIVE in LIVE_CALLS
  2. Creates entry in LIVE_QUEUE for real-time monitoring
  3. Initializes risk assessment fields

call.hangup

When a call ends, the system performs comprehensive analysis:
  1. Transcribes full audio using Deepgram batch API
  2. Analyzes emotional state using LLM
  3. Classifies service type (EMS/FIRE/POLICE/OTHER) and extracts tags
  4. Computes risk level based on:
    • Audio distress score
    • Emotional analysis
    • Semantic tags (life-threatening conditions)
  5. Generates call summary
  6. Creates queue item with status OPEN
  7. Saves complete call record
  8. Removes from LIVE_QUEUE (streaming ended)

Call Record Structure

When a call ends, the system creates a comprehensive record:
{
  "call_id": "abc123-def456-ghi789",
  "call_control_id": "v3:xyz789-abc123",
  "from_masked": "•••1234",
  "to": "+15555559911",
  "duration_seconds": 135,
  "audio": {
    "sample_rate": 8000,
    "voiced_seconds": 87.3,
    "distress_score": 0.72,
    "distress_max": 0.89,
    "distress_bucket": "HIGH_RISK"
  },
  "nlp": {
    "transcript": "Help! There's a fire in the kitchen. Smoke is everywhere!",
    "emotion": {
      "label": "DISTRESSED",
      "intensity": 0.85,
      "sentiment": "negative"
    },
    "category": "FIRE",
    "category_confidence": 0.95,
    "tags": ["FIRE", "SMOKE"],
    "summary": "Caller reports active fire in kitchen with heavy smoke"
  },
  "risk": {
    "level": "CRITICAL",
    "score": 0.89
  },
  "routing": {
    "priority": "CRITICAL",
    "recommended_unit": "FIRE",
    "recommended_tags": ["FIRE", "SMOKE"]
  },
  "ranking": {
    "weight": 103,
    "score": 0.89,
    "created_at": "2026-03-03T23:17:45Z"
  }
}

Error Responses

status_code
number
HTTP status code
detail
string
Error message

Error Response (400 Bad Request)

{
  "detail": "missing call identifiers"
}
Returned when call_control_id or call_session_id is missing from a call.initiated event.

Implementation Notes

  • Location: app/main.py:353-626
  • Uses background tasks for async operations (answering calls, starting streams)
  • Masks phone numbers (shows only last 4 digits)
  • Stores WAV files in data/calls/ directory
  • Uses in-memory storage (dev mode) or database (prod mode)

Risk Level Computation

The system computes risk levels using:
  1. Audio Analysis: RMS-based distress scoring from voice characteristics
  2. Emotional State: LLM-based emotion classification
  3. Semantic Tags: Critical conditions override risk score
Critical Tags (force CRITICAL level):
  • ACTIVE_SHOOTER, GUNSHOT, STABBING
  • NOT_BREATHING, CARDIAC_ARREST, UNCONSCIOUS
  • SEVERE_BLEEDING, OVERDOSE, SUICIDE_ATTEMPT, EXPLOSION
Elevated Tags (force at least ELEVATED):
  • TRAUMA, VIOLENCE, ASSAULT, DOMESTIC_VIOLENCE
  • FIRE, SMOKE, GAS_LEAK
  • CARDIAC_EVENT, STROKE, SEIZURE, BREATHING_DIFFICULTY, CHEST_PAIN
  • VEHICLE_ACCIDENT

cURL Example

curl -X POST http://localhost:8000/api/v1/call/incoming \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "event_type": "call.initiated",
      "payload": {
        "call_session_id": "test-call-123",
        "call_control_id": "v3:control-456",
        "from": "+15555551234",
        "to": "+15555559911",
        "start_time": "2026-03-03T23:15:30.000Z"
      }
    }
  }'

Build docs developers (and LLMs) love