Overview
Wirexa API is a self-hosted telephony platform compatible with the SignalWire and Twilio REST API and XML (LaML) verb format. You can use existing SignalWire or Twilio SDKs without modification — just point them at your engine's base URL.
Base URL: http://wirexaapi.com
API prefix: /api/laml/2010-04-01/Accounts/{ProjectID}/
Authentication
All API requests use HTTP Basic Auth. Your Project ID is the username and your Auth Token is the password.
curl https://your-engine.com/api/laml/2010-04-01/Accounts/{ProjectID}/Calls.json \
-u "{ProjectID}:{AuthToken}"
In Python SDKs, pass these as project_id and auth_token (SignalWire) or
account_sid and auth_token (Twilio).
SDK
Install the official Wirexa API SDK with pip — no files to copy, no manual setup.
pip install wirexaapi
If you were using signalwire_replacement.py or the official SignalWire SDK, just change the import — nothing else changes:
# Before (SignalWire official SDK or signalwire_replacement.py) from signalwire.rest import Client as SignalWireClient # After — rest of your code stays the same from wirexaapi import Client as SignalWireClient
Quick Start
With settings.json (recommended)
{
"signalwire": {
"project_id": "your_project_id",
"api_token": "PTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"space_url": "wirexaapi.com"
}
}
import json
from wirexaapi import Client as SignalWireClient
with open("settings.json") as f:
jsonsettings = json.load(f)
sw = jsonsettings["signalwire"]
signalwire_client = SignalWireClient(
sw["project_id"],
sw["api_token"],
signalwire_space_url=sw["space_url"],
)
call = signalwire_client.calls.create(
to="+13125551234",
from_="+18005550100",
url="https://your-backend.com/xml",
status_callback="https://your-backend.com/status",
status_callback_event=["initiated", "ringing", "completed"],
recording_status_callback="https://your-backend.com/recording-status",
machine_detection="Enable",
record=True,
)
print(call.sid, call.status) # CAxxxxxxxx queued
Minimal XML backend (Python/Flask)
from flask import Flask, request, Response
app = Flask(__name__)
@app.route("/xml", methods=["POST"])
def xml_response():
xml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Hello! This call is being recorded.</Say>
<Gather numDigits="1" timeout="5" action="/gather">
<Say>Press 1 for sales, 2 for support.</Say>
</Gather>
<Hangup/>
</Response>"""
return Response(xml, mimetype="text/xml")
@app.route("/gather", methods=["POST"])
def gather():
digit = request.form.get("Digits", "")
xml = f"<Response><Say>You pressed {digit}.</Say><Hangup/></Response>"
return Response(xml, mimetype="text/xml")
@app.route("/status", methods=["POST"])
def status():
print(request.form) # CallSid, CallStatus, AnsweredBy, etc.
return "OK"
Create a Call
POST/api/laml/2010-04-01/Accounts/{ProjectID}/Calls.json
curl -X POST "http://wirexaapi.com/api/laml/2010-04-01/Accounts/{ProjectID}/Calls.json" \
-u "{ProjectID}:{AuthToken}" \
--data-urlencode "To=+13125551234" \
--data-urlencode "From=+18005550100" \
--data-urlencode "Url=https://your-backend.com/xml" \
--data-urlencode "StatusCallback=https://your-backend.com/status" \
--data-urlencode "MachineDetection=Enable" \
--data-urlencode "Record=true" \
--data-urlencode "RecordingStatusCallback=https://your-backend.com/recording-status"
Response
{
"sid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"account_sid": "your_project_id",
"to": "+13125551234",
"from": "+18005550100",
"status": "queued",
"direction": "outbound-api",
"url": "https://your-backend.com/xml"
}
Call Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| To | string | Yes | Destination number, E.164 format (e.g. +13125551234) |
| From | string | Yes | Caller ID. Must be configured on your SIP trunk. |
| Url | string | Yes | URL that returns LaML/XML instructions for the call flow. |
| Method | string | No | HTTP method to fetch Url. Default: POST |
| StatusCallback | string | No | URL to receive call lifecycle events. |
| StatusCallbackMethod | string | No | HTTP method for StatusCallback. Default: POST |
| StatusCallbackEvent | string[] | No | Subset of events: initiated, ringing, answered, completed. Default: all. |
| MachineDetection | string | No | Enable to activate AMD. Result in AnsweredBy webhook field. |
| Record | boolean | No | true to record the full call as WAV + MP3. |
| RecordingStatusCallback | string | No | URL to receive recording completion webhook. |
| RecordingStatusCallbackMethod | string | No | HTTP method for recording callback. Default: POST |
| Timeout | integer | No | Seconds to wait for answer before marking as no-answer. Default: 60. |
List & Inspect Calls
GET/api/laml/2010-04-01/Accounts/{ProjectID}/Calls.json
# List all calls (most recent first)
curl "http://wirexaapi.com/api/laml/2010-04-01/Accounts/{ProjectID}/Calls.json" \
-u "{ProjectID}:{AuthToken}"
# Filter by status
curl "http://wirexaapi.com/api/laml/2010-04-01/Accounts/{ProjectID}/Calls.json?Status=completed&PageSize=50" \
-u "{ProjectID}:{AuthToken}"
# Get a specific call
curl "http://wirexaapi.com/api/laml/2010-04-01/Accounts/{ProjectID}/Calls/{CallSid}.json" \
-u "{ProjectID}:{AuthToken}"
Query parameters
| Parameter | Description |
|---|---|
| Status | Filter: queued | ringing | in-progress | completed | failed | busy | no-answer |
| PageSize | Number of results (default 50, max 1000) |
| Page | Page number (0-indexed) |
Modify a Call
POST/api/laml/2010-04-01/Accounts/{ProjectID}/Calls/{CallSid}.json
Modify an in-progress call. Currently supported: hangup.
# Hang up a call
curl -X POST "http://wirexaapi.com/api/laml/2010-04-01/Accounts/{ProjectID}/Calls/{CallSid}.json" \
-u "{ProjectID}:{AuthToken}" \
--data-urlencode "Status=completed"
<Say>
Converts text to speech and plays it to the caller. Supports multiple TTS providers via the voice attribute.
<Response>
<Say language="en-US" voice="polly.Joanna" loop="1">
Hello! Thank you for calling.
</Say>
</Response>
| Attribute | Default | Description |
|---|---|---|
| language | en-US | BCP-47 language tag. Passed to the TTS provider. |
| voice | woman | Voice identifier. Format: provider.VoiceId. See TTS Providers. |
| loop | 1 | Number of times to repeat. 0 = infinite (until DTMF or hangup). |
<Play>
Plays an audio file (WAV or MP3) to the caller.
<Response> <Play loop="1">https://example.com/greeting.wav</Play> </Response>
| Attribute | Default | Description |
|---|---|---|
| loop | 1 | Number of times to repeat. 0 = infinite. |
<Gather>
Collects DTMF digits from the caller. Nest <Say> or <Play> inside to play
a prompt — digits pressed during the prompt are captured immediately.
<Response>
<Gather numDigits="1" timeout="7"
action="https://your-backend.com/gather" method="POST">
<Say>Press 1 for sales, 2 for support, or 3 to repeat.</Say>
</Gather>
<!-- Fallback if no digits collected: -->
<Say>We did not receive your input. Goodbye.</Say>
<Hangup/>
</Response>
| Attribute | Default | Description |
|---|---|---|
| numDigits | — | Stop collecting after exactly N digits. Omit for open-ended collection. |
| timeout | 5 | Seconds to wait for the first digit after the prompt ends. |
| finishOnKey | # | Key that ends collection immediately (not included in Digits). |
| action | current URL | URL to POST collected digits to. If omitted, re-POSTs to the current URL. |
| method | POST | HTTP method for the action URL. |
Action request fields
| Field | Description |
|---|---|
| Digits | Collected digit string, e.g. 1 or 1234 |
| CallSid | Unique call identifier |
| From | Caller number |
| To | Destination number |
| AccountSid | Your Project ID |
<Record>
Records the caller's audio. The recording is saved as WAV and converted to MP3.
<Response>
<Say>Please leave your message after the tone.</Say>
<Record maxLength="60" silence="5"
action="https://your-backend.com/after-record"
method="POST"/>
</Response>
| Attribute | Default | Description |
|---|---|---|
| maxLength | 3600 | Maximum recording length in seconds. |
| silence | 5 | Stop recording after N seconds of silence. |
| action | — | URL called when recording is complete. Receives RecordingUrl, RecordingDuration. |
| method | POST | HTTP method for action URL. |
| playBeep | true | Play a beep before recording starts. |
<Dial>
Bridges the call to another number.
<Response>
<Dial timeout="30" callerId="+18005550100">
+13125551234
</Dial>
</Response>
| Attribute | Default | Description |
|---|---|---|
| timeout | 30 | Seconds to wait for the dialed party to answer. |
| callerId | original From | Caller ID shown to the dialed party. |
| action | — | URL to call when the dialed party hangs up. |
<Redirect> / <Pause> / <Hangup>
<Response> <!-- Pause for 2 seconds --> <Pause length="2"/> <!-- Redirect to a new URL (replaces remaining verbs) --> <Redirect method="GET">https://your-backend.com/next-step</Redirect> <!-- Hang up the call --> <Hangup/> </Response>
| Verb | Attribute | Description |
|---|---|---|
| <Pause> | length (default: 1) | Pause execution for N seconds. Caller hears silence. |
| <Redirect> | method (default: POST) | Fetch new XML from the given URL and continue execution from there. |
| <Hangup> | — | Immediately hang up the call. |
TTS Providers
Set the voice attribute on <Say> using the format provider.VoiceId.
If the specified provider is unavailable, the engine falls back automatically:
Polly → Google → ElevenLabs → Azure → espeak (system).
| voice value | Provider | Notes |
|---|---|---|
| polly.Joanna | Amazon Polly | English (US), female |
| polly.Miguel | Amazon Polly | Spanish (ES), male |
| google.en-US-Wavenet-F | Google Cloud TTS | English (US), female, WaveNet quality |
| eleven.Rachel | ElevenLabs | High-quality neural voice |
| azure.en-US-JennyNeural | Azure Cognitive Services | English (US), neural |
| woman | Auto (best available) | Tries each provider in order |
| man | Auto (best available) | Tries each provider in order |
espeak is the last-resort fallback
and produces robotic quality audio.
Answering Machine Detection (AMD)
Enable AMD by passing MachineDetection=Enable when creating the call.
The engine captures the first ~4 seconds of audio and classifies it using WebRTC VAD.
Result values
| AnsweredBy | Meaning | Confidence threshold |
|---|---|---|
| human | Short speech bursts, pauses, or initial silence before speaking — human pattern | ≥ 0.65 |
| machine | Long continuous speech (>2.4 s) from call start — voicemail greeting pattern | ≥ 0.70 |
| unknown | No audio, or ambiguous pattern within 4 s analysis window | — |
The result is delivered in the AnsweredBy field of the answered status callback.
Recordings
Enable recording by passing Record=true when creating the call, or using the <Record>
verb in your XML. Recordings are saved as WAV on disk and converted to MP3 via ffmpeg.
Download recordings
# By recording SID
curl "http://wirexaapi.com/recordings/{RecordingSid}.mp3" -o call.mp3
curl "http://wirexaapi.com/recordings/{RecordingSid}.wav" -o call.wav
# By call SID (first completed recording)
curl "http://wirexaapi.com/recordings/call/{CallSid}.mp3" -o call.mp3
# Recording metadata (JSON)
curl "http://wirexaapi.com/2010-04-01/Accounts/{ProjectID}/Recordings/{RecordingSid}.json"
Recording status webhook fields
| Field | Example | Description |
|---|---|---|
| RecordingSid | RExxxxx | Unique recording identifier |
| CallSid | CAxxxxx | Parent call SID |
| RecordingStatus | completed | processing | completed | failed |
| RecordingUrl | http://.../recordings/RExxxxx | Base URL. Append .mp3 or .wav to download. |
| RecordingDuration | 45 | Duration in seconds |
| RecordingChannels | mono | mono or stereo |
| AccountSid | your_project_id | Your Project ID |
Webhooks
Status callbacks are sent as HTTP POST with application/x-www-form-urlencoded body.
Failed deliveries are retried with exponential backoff and persist across engine restarts.
Retry schedule
| Attempt | Delay |
|---|---|
| 1st (immediate) | 0 s |
| 2nd | 60 s |
| 3rd | 5 min |
| 4th | 15 min |
| After 4 failures | Marked as failed — no more retries |
Call status webhook fields
| Field | Example | Description |
|---|---|---|
| CallSid | CAxxxxx | Unique call identifier |
| CallStatus | completed | initiated | ringing | answered | completed | failed | busy | no-answer |
| To | +13125551234 | Destination number |
| From | +18005550100 | Caller ID |
| Direction | outbound-api | Call direction |
| CallDuration | 45 | Duration in seconds (included on completed) |
| AnsweredBy | human | AMD result: human | machine | unknown (only when AMD enabled) |
| AccountSid | your_project_id | Your Project ID |
| Timestamp | 2026-03-06T12:00:00Z | ISO 8601 event time |
RecordingStatusCallback (with RecordingUrl, RecordingSid, etc.)
and (2) call status callback → StatusCallback (with CallStatus, AnsweredBy, etc.).
They do not overlap.
Error Codes
| HTTP Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Resource created |
| 202 | Accepted — resource still processing (e.g. recording converting to MP3) |
| 400 | Bad request — missing or invalid parameters |
| 401 | Unauthorized — invalid or missing credentials |
| 402 | Payment required — account has no credits |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
| 503 | Service temporarily unavailable |
Error response format
{
"detail": "No credits remaining for this project"
}
IVR Backend — Overview
Your application acts as a webhook server. The Voice Engine calls your endpoints to get XML instructions and to deliver event notifications. Your app also calls the Engine's REST API to create outbound calls.
1. Your backend →
POST /Calls.json → Engine creates the call2. Engine →
POST /xml → Your backend returns XML instructions3. Engine executes XML (Say, Gather, Record…)
4. Engine →
POST /gather → Your backend receives digits, returns new XML5. Engine →
POST /amd → Your backend receives AMD result (if enabled)6. Engine →
POST /status → Your backend receives final call status
ngrok http 9000 → copy the HTTPS URL as MY_PUBLIC_URL
Setup & Config
Install dependencies
pip install fastapi uvicorn httpx python-multipart
Environment variables (.env)
# The Voice Engine base URL ENGINE_URL=https://wirexaapi.com # Your credentials — copy from the Dashboard ACCOUNT_SID=your_project_id AUTH_TOKEN=your_api_token # Public URL of YOUR backend (Engine needs this to reach your webhooks) # Use ngrok during local development: ngrok http 9000 MY_PUBLIC_URL=https://your-backend.example.com
Run
uvicorn app:app --port 9000 --reload
1. Create a Call
Call the Engine's REST API to start an outbound call. The Engine will call your /xml
endpoint once the call is answered to get instructions.
import os, httpx
from fastapi import FastAPI
app = FastAPI()
ENGINE_URL = os.getenv("ENGINE_URL", "https://wirexaapi.com")
ACCOUNT_SID = os.getenv("ACCOUNT_SID", "your_project_id")
AUTH_TOKEN = os.getenv("AUTH_TOKEN", "your_api_token")
MY_PUBLIC_URL = os.getenv("MY_PUBLIC_URL","https://your-backend.example.com")
@app.post("/call")
async def create_call(to: str, from_number: str = "+10000000000"):
async with httpx.AsyncClient(timeout=15) as client:
r = await client.post(
f"{ENGINE_URL}/api/laml/2010-04-01/Accounts/{ACCOUNT_SID}/Calls.json",
auth=(ACCOUNT_SID, AUTH_TOKEN),
data={
"To": to,
"From": from_number,
"Url": f"{MY_PUBLIC_URL}/xml", # (2)
"Method": "POST",
"StatusCallback": f"{MY_PUBLIC_URL}/status", # (6)
"StatusCallbackMethod": "POST",
# Optional: answering machine detection
"MachineDetection": "Enable",
"AsyncAmd": "true",
"AsyncAmdStatusCallback": f"{MY_PUBLIC_URL}/amd", # (5)
},
)
data = r.json()
return {"call_sid": data.get("sid"), "status": data.get("status")}
2. Serve XML Instructions
When the call is answered the Engine sends a POST to your /xml URL.
Respond with valid LaML/XML to control the call.
Fields sent by the Engine: CallSid, CallStatus, To,
From, Direction, AnsweredBy.
from fastapi import Request
from fastapi.responses import PlainTextResponse
@app.post("/xml", response_class=PlainTextResponse)
async def xml_instructions(request: Request) -> str:
form = dict(await request.form())
call_sid = form.get("CallSid", "")
answered_by = form.get("AnsweredBy", "")
# Route differently if a machine answered
if answered_by == "machine_start":
return """<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Please call us back at our office. Thank you.</Say>
<Hangup/>
</Response>"""
# Human answered — play IVR menu
return f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Hello, thank you for answering.</Say>
<Gather numDigits="1" timeout="7"
action="{MY_PUBLIC_URL}/gather" method="POST">
<Say>
Press 1 for sales.
Press 2 for support.
Press 9 to be removed from our list.
</Say>
</Gather>
<Say>We did not receive your selection. Goodbye.</Say>
<Hangup/>
</Response>"""
3. Handle Gather (Digits)
After the caller presses a key, the Engine sends a POST to your /gather
URL. Respond with XML to continue the call.
Fields: CallSid, Digits, From, To.
@app.post("/gather", response_class=PlainTextResponse)
async def gather(request: Request) -> str:
form = dict(await request.form())
digits = form.get("Digits", "")
if digits == "1":
return """<Response>
<Say>Connecting you to sales. Please hold.</Say>
<Transfer timeout="30">+13125550100</Transfer>
</Response>"""
elif digits == "2":
return """<Response>
<Say>Our support team will email you shortly. Goodbye.</Say>
<Hangup/>
</Response>"""
elif digits == "9":
# TODO: mark number as do-not-call in your database
return """<Response>
<Say>You have been removed from our list. Goodbye.</Say>
<Hangup/>
</Response>"""
else:
# Unrecognised digit — repeat menu
return f"""<Response>
<Gather numDigits="1" timeout="7"
action="{MY_PUBLIC_URL}/gather" method="POST">
<Say>Invalid option. Press 1 for sales, 2 for support.</Say>
</Gather>
<Hangup/>
</Response>"""
4. AMD Result
When MachineDetection=Enable and AsyncAmd=true are set, the Engine
sends AMD results here asynchronously — typically within 2–5 seconds of the call being answered.
Fields: CallSid, AnsweredBy (human | machine_start | unknown),
AmdConfidence, AmdDetectionTimeMs.
@app.post("/amd")
async def amd_result(request: Request) -> dict:
form = dict(await request.form())
call_sid = form.get("CallSid", "")
answered_by = form.get("AnsweredBy", "") # human | machine_start | unknown
confidence = form.get("AmdConfidence", "")
time_ms = form.get("AmdDetectionTimeMs", "")
print(f"[{call_sid}] AMD: {answered_by} (confidence={confidence}, {time_ms}ms)")
if answered_by == "machine_start":
# Optionally redirect the call to leave a voicemail
async with httpx.AsyncClient(timeout=10) as client:
await client.post(
f"{ENGINE_URL}/api/laml/2010-04-01/Accounts/{ACCOUNT_SID}/Calls/{call_sid}.json",
auth=(ACCOUNT_SID, AUTH_TOKEN),
data={"Url": f"{MY_PUBLIC_URL}/xml/voicemail", "Method": "POST"},
)
return {"received": True}
5. Status Callback
Fires at call end (and optionally at answered, initiated, ringing).
Use this to update your database, trigger billing, etc.
Fields: CallSid, CallStatus, To, From,
Direction, CallDuration, AnsweredBy.
@app.post("/status")
async def status_callback(request: Request) -> dict:
form = dict(await request.form())
sid = form.get("CallSid", "")
status = form.get("CallStatus", "")
duration = form.get("CallDuration", "0")
print(f"[{sid}] status={status} duration={duration}s")
if status == "completed":
pass # update DB, send report, etc.
elif status == "failed":
pass # retry logic, alert, etc.
elif status == "no-answer":
pass # reschedule, etc.
return {"received": True}
RecordingStatusCallback
with RecordingUrl, RecordingSid, and RecordingDuration.
It does not include CallStatus — keep them separate in your logic.
Full Example
Complete app.py — ready to run. Copy, set your .env, and start with
uvicorn app:app --port 9000 --reload.
"""
IVR backend example — connects to the Wirexa Voice Engine.
Install: pip install fastapi uvicorn httpx python-multipart
Run: uvicorn app:app --port 9000 --reload
"""
from __future__ import annotations
import logging, os
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
app = FastAPI()
ENGINE_URL = os.getenv("ENGINE_URL", "https://wirexaapi.com")
ACCOUNT_SID = os.getenv("ACCOUNT_SID", "your_project_id")
AUTH_TOKEN = os.getenv("AUTH_TOKEN", "your_api_token")
MY_PUBLIC_URL = os.getenv("MY_PUBLIC_URL", "https://your-backend.example.com")
AUTH = (ACCOUNT_SID, AUTH_TOKEN)
CALLS_URL = f"{ENGINE_URL}/api/laml/2010-04-01/Accounts/{ACCOUNT_SID}/Calls.json"
# ── 1. CREATE A CALL ──────────────────────────────────────────────────────────
@app.post("/call")
async def create_call(to: str, from_number: str = "+10000000000") -> dict:
async with httpx.AsyncClient(timeout=15) as client:
r = await client.post(CALLS_URL, auth=AUTH, data={
"To": to,
"From": from_number,
"Url": f"{MY_PUBLIC_URL}/xml",
"Method": "POST",
"StatusCallback": f"{MY_PUBLIC_URL}/status",
"StatusCallbackMethod": "POST",
"MachineDetection": "Enable",
"AsyncAmd": "true",
"AsyncAmdStatusCallback": f"{MY_PUBLIC_URL}/amd",
})
data = r.json()
log.info("Call created: %s → %s", data.get("sid"), to)
return {"call_sid": data.get("sid"), "status": data.get("status")}
# ── 2. SERVE XML ──────────────────────────────────────────────────────────────
@app.post("/xml", response_class=PlainTextResponse)
async def xml_instructions(request: Request) -> str:
form = dict(await request.form())
log.info("[%s] XML requested — answered_by=%s", form.get("CallSid"), form.get("AnsweredBy"))
return f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Hello, thank you for answering.</Say>
<Gather numDigits="1" timeout="7"
action="{MY_PUBLIC_URL}/gather" method="POST">
<Say>
Press 1 for sales.
Press 2 for support.
Press 9 to be removed from our list.
</Say>
</Gather>
<Say>We did not receive your selection. Goodbye.</Say>
<Hangup/>
</Response>"""
# ── 3. HANDLE GATHER ─────────────────────────────────────────────────────────
@app.post("/gather", response_class=PlainTextResponse)
async def gather(request: Request) -> str:
form = dict(await request.form())
digits = form.get("Digits", "")
log.info("[%s] Gather: digits=%r", form.get("CallSid"), digits)
if digits == "1":
return """<Response>
<Say>Connecting you to sales.</Say>
<Transfer timeout="30">+13125550100</Transfer>
</Response>"""
elif digits == "2":
return """<Response>
<Say>Our team will contact you. Goodbye.</Say>
<Hangup/>
</Response>"""
elif digits == "9":
return """<Response>
<Say>You have been removed from our list. Goodbye.</Say>
<Hangup/>
</Response>"""
else:
return f"""<Response>
<Gather numDigits="1" timeout="7"
action="{MY_PUBLIC_URL}/gather" method="POST">
<Say>Invalid option. Press 1 for sales, 2 for support.</Say>
</Gather>
<Hangup/>
</Response>"""
# ── 4. AMD RESULT ─────────────────────────────────────────────────────────────
@app.post("/amd")
async def amd_result(request: Request) -> dict:
form = dict(await request.form())
call_sid = form.get("CallSid", "")
answered_by = form.get("AnsweredBy", "")
log.info("[%s] AMD: %s (%sms)", call_sid, answered_by, form.get("AmdDetectionTimeMs"))
# Redirect to voicemail XML if machine answered
if answered_by == "machine_start":
async with httpx.AsyncClient(timeout=10) as client:
await client.post(
f"{ENGINE_URL}/api/laml/2010-04-01/Accounts/{ACCOUNT_SID}/Calls/{call_sid}.json",
auth=AUTH,
data={"Url": f"{MY_PUBLIC_URL}/xml/voicemail", "Method": "POST"},
)
return {"received": True}
# ── 5. STATUS CALLBACK ────────────────────────────────────────────────────────
@app.post("/status")
async def status_callback(request: Request) -> dict:
form = dict(await request.form())
sid = form.get("CallSid", "")
status = form.get("CallStatus", "")
log.info("[%s] status=%s duration=%ss", sid, status, form.get("CallDuration", "0"))
# TODO: update your database here
return {"received": True}
# ── HEALTH ────────────────────────────────────────────────────────────────────
@app.get("/health")
async def health() -> dict:
try:
async with httpx.AsyncClient(timeout=5) as c:
r = await c.get(f"{ENGINE_URL}/health")
engine_ok = r.status_code == 200
except Exception:
engine_ok = False
return {"backend": "ok", "engine_connected": engine_ok}