Loading...
Loading...
Voice: outbound/inbound, TwiML, conferencing, recording, transcription, IVR Gather, SIP, BYOC
npx skill4agent add alphaonedev/openclaw-graph twilio-voice<Dial><Conference><Gather><Record><Say>AC...twilio@4.23.0twilio==9.0.5TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN<Dial>CallSid<Dial>ParentCallSidCallSidCallSidinitiatedringingansweredcompleted<Gather><Dial><Conference><Record>ashburndublinsingaporepip install twiliofrom twilio.rest import Client
from twilio.twiml.voice_response import VoiceResponse
client = Client()
# Outbound call
call = client.calls.create(
url="https://demo.twilio.com/docs/voice.xml",
to="+15558675309",
from_="+15017250604"
)
print(call.sid)
# TwiML response (in webhook handler)
resp = VoiceResponse()
resp.say("Hello from Twilio Python!")
resp.record(transcribe=True, transcribe_callback="/transcription")sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # v20.11.1
npm -v # 10.xsudo apt-get update
sudo apt-get install -y python3.11 python3.11-venv python3-pip
python3.11 -V # Python 3.11.8curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
| sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" \
| sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt-get update && sudo apt-get install -y ngrok
ngrok version # 3.14.2sudo dnf install -y nodejs20 jq python3.11 python3.11-pip
node -v
python3.11 -Vcurl -L -o /tmp/ngrok.tgz https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
sudo tar -C /usr/local/bin -xzf /tmp/ngrok.tgz ngrok
ngrok versionbrew update
brew install node@20 python@3.11 jq ngrok/ngrok/ngrok
node -v
python3.11 -V
ngrok versionecho 'export PATH="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc # Apple Silicon
echo 'export PATH="/usr/local/opt/node@20/bin:$PATH"' >> ~/.zshrc # Intel
source ~/.zshrc/srv/twilio-voice/~/src/twilio-voice//etc/twilio/voice.env./.envmkdir -p ~/src/twilio-voice && cd ~/src/twilio-voice
npm init -y
npm install twilio@4.23.0 express@4.18.3 body-parser@1.20.2 pino@9.0.0server.jsconst express = require("express");
const bodyParser = require("body-parser");
const twilio = require("twilio");
const pino = require("pino");
const log = pino({ level: process.env.LOG_LEVEL || "info" });
const app = express();
// Twilio sends application/x-www-form-urlencoded by default
app.use(bodyParser.urlencoded({ extended: false }));
// Optional: validate Twilio signature (recommended in production)
const validateTwilio = (req, res, next) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.header("X-Twilio-Signature");
const url = `${process.env.PUBLIC_BASE_URL}${req.originalUrl}`;
const isValid = twilio.validateRequest(authToken, signature, url, req.body);
if (!isValid) return res.status(403).send("Invalid Twilio signature");
next();
};
app.post("/voice/inbound", validateTwilio, (req, res) => {
const vr = new twilio.twiml.VoiceResponse();
// Example: simple IVR greeting + gather
const gather = vr.gather({
input: "dtmf",
numDigits: 1,
timeout: 5,
action: "/voice/menu",
method: "POST",
});
gather.say(
{ voice: "Polly.Joanna", language: "en-US" },
"Press 1 for sales. Press 2 for support."
);
vr.say({ voice: "Polly.Joanna", language: "en-US" }, "No input received. Goodbye.");
vr.hangup();
res.type("text/xml").send(vr.toString());
});
app.post("/voice/menu", validateTwilio, (req, res) => {
const digit = req.body.Digits;
const vr = new twilio.twiml.VoiceResponse();
if (digit === "1") {
vr.dial({ callerId: process.env.TWILIO_CALLER_ID }, "+14155550100");
} else if (digit === "2") {
vr.dial({ callerId: process.env.TWILIO_CALLER_ID }, "+14155550101");
} else {
vr.say({ voice: "Polly.Joanna", language: "en-US" }, "Invalid choice.");
vr.redirect({ method: "POST" }, "/voice/inbound");
}
res.type("text/xml").send(vr.toString());
});
app.get("/healthz", (req, res) => res.status(200).send("ok"));
const port = Number(process.env.PORT || 3000);
app.listen(port, () => log.info({ port }, "twilio-voice server listening"));export TWILIO_AUTH_TOKEN="your_auth_token"
export PUBLIC_BASE_URL="https://example.ngrok-free.app"
export TWILIO_CALLER_ID="+14155551234"
node server.jsngrok http 3000
# copy the https URL into PUBLIC_BASE_URL and Twilio Console webhookmkdir -p ~/src/twilio-voice-py && cd ~/src/twilio-voice-py
python3.11 -m venv .venv
source .venv/bin/activate
pip install "twilio==9.0.5" "fastapi==0.109.2" "uvicorn[standard]==0.27.1"app.pyimport os
from fastapi import FastAPI, Request, Response
from twilio.twiml.voice_response import VoiceResponse, Gather
from twilio.request_validator import RequestValidator
app = FastAPI()
def validate_twilio(request: Request, form: dict) -> None:
auth_token = os.environ["TWILIO_AUTH_TOKEN"]
validator = RequestValidator(auth_token)
signature = request.headers.get("X-Twilio-Signature", "")
url = os.environ["PUBLIC_BASE_URL"] + str(request.url.path)
if not validator.validate(url, form, signature):
raise PermissionError("Invalid Twilio signature")
@app.post("/voice/inbound")
async def inbound(request: Request):
form = dict(await request.form())
validate_twilio(request, form)
vr = VoiceResponse()
gather = Gather(input="dtmf", num_digits=1, timeout=5, action="/voice/menu", method="POST")
gather.say("Press 1 for sales. Press 2 for support.", voice="Polly.Joanna", language="en-US")
vr.append(gather)
vr.say("No input received. Goodbye.", voice="Polly.Joanna", language="en-US")
vr.hangup()
return Response(content=str(vr), media_type="text/xml")
@app.post("/voice/menu")
async def menu(request: Request):
form = dict(await request.form())
validate_twilio(request, form)
digit = form.get("Digits")
vr = VoiceResponse()
if digit == "1":
vr.dial("+14155550100", caller_id=os.environ["TWILIO_CALLER_ID"])
elif digit == "2":
vr.dial("+14155550101", caller_id=os.environ["TWILIO_CALLER_ID"])
else:
vr.say("Invalid choice.", voice="Polly.Joanna", language="en-US")
vr.redirect("/voice/inbound", method="POST")
return Response(content=str(vr), media_type="text/xml")export TWILIO_AUTH_TOKEN="your_auth_token"
export PUBLIC_BASE_URL="https://example.ngrok-free.app"
export TWILIO_CALLER_ID="+14155551234"
uvicorn app:app --host 0.0.0.0 --port 3000X-Twilio-Signatureapplication/x-www-form-urlencodedContent-Type: text/xmlPOST /voice/inboundtofromurltwimlmachineDetection<Gather>timeoutspeechTimeoutlanguage<Conference>startConferenceOnEnterendConferenceOnExitbeeprecord<Dial record="record-from-answer"><Conference record="record-from-start"><Record>CallSidRecordingSidbrew install twilio/brew/twilio
twilio --versionsudo npm install -g twilio-cli@5.20.0
twilio --versionsudo npm install -g twilio-cli@5.20.0
twilio --versiontwilio login
# prompts for Account SID and Auth Tokenexport TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"twilio api:core:calls:list \
--limit 50 \
--properties sid,from,to,status,startDate,endDate,duration,price,priceUnit--limit <n>--properties <csv>twilio api:core:calls:fetch --sid YOUR_CA_SID--sid <CA...>twilio api:core:calls:create \
--from +14155551234 \
--to +14155559876 \
--url https://voice.example.com/twiml/outbound \
--status-callback https://voice.example.com/webhooks/voice/status \
--status-callback-event initiated ringing answered completed \
--status-callback-method POST \
--timeout 20--from <E.164|client:...|sip:...>--to <E.164|client:...|sip:...>--url <https://...>--twiml--twiml <xml>--method <GET|POST>url--status-callback <url>--status-callback-event <events...>--status-callback-method <GET|POST>--timeout <seconds>--machine-detection <Enable|DetectMessageEnd|...>--record--recording-status-callback <url>--recording-status-callback-method <GET|POST>twilio api:core:recordings:list --limit 20 \
--properties sid,callSid,dateCreated,duration,price,priceUnit,statustwilio api:core:recordings:fetch --sid RE3f3b1b2c0d0f4a5b6c7d8e9f0a1b2c3export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
-L "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Recordings/RE3f3b1b2c0d0f4a5b6c7d8e9f0a1b2c3.wav" \
-o recording.wav
file recording.wavtwilio api:core:conferences:list --status in-progress --limit 20
twilio api:core:conferences:fetch --sid YOUR_CF_SIDtwilio api:core:conferences:participants:list \
--conference-sid YOUR_CF_SID \
--limit 50twilio api:core:conferences:participants:update \
--conference-sid YOUR_CF_SID \
--sid CAbcdef0123456789abcdef0123456789 \
--muted true--muted <true|false>--hold <true|false>--hold-url <url>twilio api:core:accounts:fetch
twilio api:request --method GET \
--uri "/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls.json?PageSize=20"--method <GET|POST|PUT|DELETE>--uri <path>https://api.twilio.com--data <k=v>/etc/twilio/voice.env# Twilio auth
TWILIO_ACCOUNT_SID=AC2f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
TWILIO_AUTH_TOKEN=9f8e7d6c5b4a3a2b1c0d9e8f7a6b5c4d
# Public base URL used for signature validation
PUBLIC_BASE_URL=https://voice.prod.example.com
# Caller ID must be a Twilio number or verified caller ID
TWILIO_CALLER_ID=+14155551234
# Webhook behavior
LOG_LEVEL=info
PORT=3000
# Recording/transcription pipeline
RECORDINGS_BUCKET=s3://prod-voice-recordings-us-east-1
TRANSCRIPTION_PROVIDER=deepgram
TRANSCRIPTION_WEBHOOK_SECRET=whsec_6b1f0b2a9c3d4e5f/etc/systemd/system/twilio-voice.service[Unit]
Description=Twilio Voice Webhook Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=twilio
Group=twilio
EnvironmentFile=/etc/twilio/voice.env
WorkingDirectory=/srv/twilio-voice
ExecStart=/usr/bin/node /srv/twilio-voice/server.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/twilio-voice
AmbientCapabilities=
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.targetsudo useradd --system --home /srv/twilio-voice --shell /usr/sbin/nologin twilio
sudo mkdir -p /srv/twilio-voice
sudo chown -R twilio:twilio /srv/twilio-voice
sudo systemctl daemon-reload
sudo systemctl enable --now twilio-voice
sudo systemctl status twilio-voice --no-pager/etc/nginx/sites-available/voice.prod.example.comserver {
listen 443 ssl http2;
server_name voice.prod.example.com;
ssl_certificate /etc/letsencrypt/live/voice.prod.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/voice.prod.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Twilio webhooks are small; keep limits tight
client_max_body_size 64k;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 10s;
}
location = /healthz {
proxy_pass http://127.0.0.1:3000/healthz;
}
}/srv/twilio-voice/twiml/outbound.xml<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="Polly.Joanna" language="en-US">Connecting your call.</Say>
<Dial callerId="+14155551234" record="record-from-answer" recordingStatusCallback="https://voice.prod.example.com/webhooks/recording">
<Number>+14155559876</Number>
</Dial>
</Response>completedCallStatus=busy|no-answer|failedflowchart LR
A[Create Call] --> B[Status Callback]
B -->|answered| C[Normal completion]
B -->|busy/no-answer/failed| D[Send SMS fallback]
D --> E[Message Status Webhook]{RecordingSid, CallSid, RecordingUrl, Timestamp}CallSidParentCallSidFromToDirectionCallSidTwilio could not find a valid URL for the Voice request11200 - HTTP retrieval failureError - 11200/healthz12300 - Invalid Content-TypeError - 12300Content-Typeapplication/jsonContent-Type: text/xml12100 - Document parse failureError - 12100&&21211 - The 'To' number +... is not a valid phone number{
"code": 21211,
"message": "The 'To' number +1415555 is not a valid phone number.",
"status": 400
}+14155551212sip:user@domain20003 - AuthenticateHTTP 401code: 2000320429 - Too Many RequestsHTTP 429code: 2042930003 - Unreachable destination handset30003403 Invalid Twilio signatureHostX-Forwarded-ProtoPUBLIC_BASE_URLHostX-Forwarded-Proto488 Not Acceptable HereX-Twilio-SignatureNoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=true+14155551234+1415****234Calls.fetchsupport-${ticketId}voice:{CallSid}:{EventType}:{Timestamp}<Gather>DigitsnumDigits<Dial>ParentCallSidedgeregionCallSid<Dial>const vr = new twilio.twiml.VoiceResponse();
vr.say({ voice: "Polly.Matthew", language: "en-US" }, "You have reached incident response.");
vr.dial(
{
callerId: process.env.TWILIO_CALLER_ID,
timeout: 20,
record: "record-from-answer",
recordingStatusCallback: "https://voice.prod.example.com/webhooks/recording",
},
"+14155550123"
);
res.type("text/xml").send(vr.toString());export TWILIO_ACCOUNT_SID="AC2f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5"
export TWILIO_AUTH_TOKEN="9f8e7d6c5b4a3a2b1c0d9e8f7a6b5c4d"
curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls.json" \
--data-urlencode "From=+14155551234" \
--data-urlencode "To=+14155559876" \
--data-urlencode "Twiml=<Response><Say voice=\"Polly.Joanna\">This is a test call.</Say><Hangup/></Response>" \
--data-urlencode "StatusCallback=https://voice.prod.example.com/webhooks/voice/status" \
--data-urlencode "StatusCallbackEvent=initiated ringing answered completed" \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN"startConferenceOnEnter=false<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial>
<Conference
startConferenceOnEnter="true"
endConferenceOnExit="true"
beep="onEnter"
record="record-from-start"
recordingStatusCallback="https://voice.prod.example.com/webhooks/recording"
>support-ticket-842193</Conference>
</Dial>
</Response><Record><?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="Polly.Joanna">Please leave a message after the tone.</Say>
<Record
maxLength="120"
timeout="5"
playBeep="true"
recordingStatusCallback="https://voice.prod.example.com/webhooks/recording"
recordingStatusCallbackMethod="POST"
/>
<Say voice="Polly.Joanna">Thank you. Goodbye.</Say>
<Hangup/>
</Response>.wavRecordingSid<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="Polly.Matthew">Routing your SIP call.</Say>
<Dial callerId="+14155551234" timeout="15">
<Number>+14155550199</Number>
</Dial>
<Say voice="Polly.Matthew">We could not connect your call.</Say>
<Hangup/>
</Response>ivr:{CallSid}{state, retries}/voice/inboundMENU/voice/menu/voice/inbound| Task | Command / API | Key flags / fields |
|---|---|---|
| Create outbound call | | |
| List calls | | |
| Fetch call | | |
| List recordings | | |
| Download recording | | |
| List conferences | | |
| List participants | | |
| Mute participant | | |
| Debug webhook failures | Twilio Console Debugger | look for |
| Validate webhook | helper libs | |
twilio-corehttp-webhookstls-nginxobservabilitytwilio-messagingtwilio-verifysendgridtwilio-studioqueue-workersplivo-voicevonage-voiceaws-connect