Files
hold-slayer/docs/api-reference.md
Robert Helewka ecf37658ce feat: add initial Hold Slayer AI telephony gateway implementation
Complete project scaffolding and core implementation of an AI-powered
telephony system that calls companies, navigates IVR menus, waits on
hold, and transfers to the user when a human answers.

Key components:
- FastAPI server with REST API, WebSocket, and MCP (SSE) interfaces
- SIP/VoIP call management via PJSUA2 with RTP audio streaming
- LLM-powered IVR navigation using OpenAI/Anthropic with tool calling
- Hold detection service combining audio analysis and silence detection
- Real-time STT (Whisper/Deepgram) and TTS (OpenAI/Piper) pipelines
- Call recording with per-channel and mixed audio capture
- Event bus (asyncio pub/sub) for real-time client updates
- Web dashboard with live call monitoring
- SQLite persistence via SQLAlchemy with call history and analytics
- Notification support (email, SMS, webhook, desktop)
- Docker Compose deployment with Opal VoIP and Opal Media containers
- Comprehensive test suite with unit, integration, and E2E tests
- Simplified .gitignore and full project documentation in README
2026-03-21 19:23:26 +00:00

7.0 KiB

API Reference

Hold Slayer exposes a REST API, WebSocket endpoint, and MCP server.

REST API

Base URL: http://localhost:8000/api

Calls

Place an Outbound Call

POST /api/calls/outbound

Request:

{
  "number": "+18005551234",
  "mode": "hold_slayer",
  "intent": "dispute Amazon charge from December 15th",
  "device": "sip_phone",
  "call_flow_id": "chase_bank_disputes",
  "services": {
    "recording": true,
    "transcription": true
  }
}

Call Modes:

Mode Description
direct Dial and connect to your device immediately
hold_slayer Navigate IVR, wait on hold, transfer when human detected
ai_assisted Connect with noise cancel, transcription, recording

Response:

{
  "call_id": "call_abc123",
  "status": "trying",
  "number": "+18005551234",
  "mode": "hold_slayer",
  "started_at": "2026-01-15T10:30:00Z"
}

Launch Hold Slayer

POST /api/calls/hold-slayer

Convenience endpoint — equivalent to POST /outbound with mode=hold_slayer.

Request:

{
  "number": "+18005551234",
  "intent": "dispute Amazon charge from December 15th",
  "call_flow_id": "chase_bank_disputes",
  "transfer_to": "sip_phone"
}

Get Call Status

GET /api/calls/{call_id}

Response:

{
  "call_id": "call_abc123",
  "status": "on_hold",
  "number": "+18005551234",
  "mode": "hold_slayer",
  "duration": 847,
  "hold_time": 780,
  "audio_type": "music",
  "transcript_excerpt": "...your call is important to us...",
  "classification_history": [
    {"timestamp": 1706000000, "type": "ringing", "confidence": 0.95},
    {"timestamp": 1706000003, "type": "ivr_prompt", "confidence": 0.88},
    {"timestamp": 1706000010, "type": "music", "confidence": 0.92}
  ],
  "services": {"recording": true, "transcription": true}
}

List Active Calls

GET /api/calls

Response:

{
  "calls": [
    {"call_id": "call_abc123", "status": "on_hold", "number": "+18005551234", "duration": 847},
    {"call_id": "call_def456", "status": "connected", "number": "+18009876543", "duration": 120}
  ],
  "total": 2
}

End a Call

POST /api/calls/{call_id}/hangup

Transfer a Call

POST /api/calls/{call_id}/transfer

Request:

{
  "device": "sip_phone"
}

Call Flows

List Call Flows

GET /api/call-flows
GET /api/call-flows?company=Chase+Bank
GET /api/call-flows?tag=banking

Response:

{
  "flows": [
    {
      "id": "chase_bank_disputes",
      "name": "Chase Bank — Disputes",
      "company": "Chase Bank",
      "phone_number": "+18005551234",
      "step_count": 7,
      "success_count": 12,
      "fail_count": 1,
      "tags": ["banking", "disputes"]
    }
  ]
}

Get Call Flow

GET /api/call-flows/{flow_id}

Returns the full call flow with all steps.

Create Call Flow

POST /api/call-flows

Request:

{
  "name": "Chase Bank — Disputes",
  "company": "Chase Bank",
  "phone_number": "+18005551234",
  "steps": [
    {"id": "wait", "type": "WAIT", "description": "Wait for greeting", "timeout": 5.0, "next_step": "menu"},
    {"id": "menu", "type": "LISTEN", "description": "Main menu", "next_step": "press3"},
    {"id": "press3", "type": "DTMF", "description": "Account services", "dtmf": "3", "next_step": "hold"},
    {"id": "hold", "type": "HOLD", "description": "Wait for agent", "next_step": "transfer"},
    {"id": "transfer", "type": "TRANSFER", "description": "Connect to user"}
  ]
}

Update Call Flow

PUT /api/call-flows/{flow_id}

Delete Call Flow

DELETE /api/call-flows/{flow_id}

Devices

List Registered Devices

GET /api/devices

Response:

{
  "devices": [
    {
      "id": "dev_001",
      "name": "Office SIP Phone",
      "type": "sip_phone",
      "sip_uri": "sip:robert@gateway.helu.ca",
      "is_online": true,
      "priority": 10
    }
  ]
}

Register a Device

POST /api/devices

Request:

{
  "name": "Office SIP Phone",
  "type": "sip_phone",
  "sip_uri": "sip:robert@gateway.helu.ca",
  "priority": 10,
  "capabilities": ["voice"]
}

Update Device

PUT /api/devices/{device_id}

Remove Device

DELETE /api/devices/{device_id}

Error Responses

All errors follow a consistent format:

{
  "detail": "Call not found: call_xyz789"
}
Status Code Meaning
400 Bad request (invalid parameters)
404 Resource not found (call, flow, device)
409 Conflict (call already ended, device already registered)
500 Internal server error

WebSocket

Event Stream

ws://localhost:8000/ws/events
ws://localhost:8000/ws/events?call_id=call_abc123
ws://localhost:8000/ws/events?types=human_detected,hold_detected

Query Parameters:

Param Description
call_id Filter events for a specific call
types Comma-separated event types to receive

Event Format:

{
  "type": "hold_detected",
  "call_id": "call_abc123",
  "timestamp": "2026-01-15T10:35:00Z",
  "data": {
    "audio_type": "music",
    "confidence": 0.92,
    "hold_duration": 0
  }
}

Event Types

Type Data Fields
call_started number, mode, intent
call_ringing number
call_connected number, duration
call_ended number, duration, reason
call_failed number, error
hold_detected audio_type, confidence
human_detected confidence, transcript_excerpt
transfer_started device, from_call_id
transfer_complete device, bridge_id
ivr_step step_id, step_type, description
ivr_dtmf_sent digits, step_id
ivr_menu_detected transcript, options
audio_classified audio_type, confidence, features
transcript_chunk text, speaker, is_final
recording_started recording_id, path
recording_stopped recording_id, duration, file_size

Client Example

const ws = new WebSocket("ws://localhost:8000/ws/events");

ws.onopen = () => {
  console.log("Connected to Hold Slayer events");
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  switch (data.type) {
    case "human_detected":
      alert("🚨 A live person picked up! Pick up your phone!");
      break;
    case "hold_detected":
      console.log("⏳ On hold...");
      break;
    case "transcript_chunk":
      console.log(`📝 ${data.data.speaker}: ${data.data.text}`);
      break;
  }
};

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};

Python Client Example

import asyncio
import websockets
import json

async def listen():
    async with websockets.connect("ws://localhost:8000/ws/events") as ws:
        async for message in ws:
            event = json.loads(message)
            print(f"[{event['type']}] {event.get('data', {})}")

asyncio.run(listen())