From twilio-developer-kit
Builds voice call logic using TwiML (Twilio Markup Language) with Python and Node.js SDKs. Covers core verbs (Say, Play, Gather, Dial, Record, Conference) and a complete inbound IVR example.
How this skill is triggered — by the user, by Claude, or both
Slash command
/twilio-developer-kit:twilio-voice-twimlThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
TwiML is XML that Twilio executes during a call. Your server returns a TwiML document in response to a Twilio webhook POST, and Twilio executes it.
TwiML is XML that Twilio executes during a call. Your server returns a TwiML document in response to a Twilio webhook POST, and Twilio executes it.
Caller → Twilio → POST to your webhook → Your server returns TwiML → Twilio executes it
twilio-account-setupContent-Type: text/xmlpip install twilio / npm install twilioA minimal inbound call handler that greets the caller and presents a menu:
Python (Flask)
from flask import Flask, request
from twilio.twiml.voice_response import VoiceResponse
app = Flask(__name__)
@app.route("/voice", methods=["POST"])
def handle_call():
response = VoiceResponse()
gather = response.gather(num_digits=1, action="/menu-choice")
gather.say("Welcome to Acme. Press 1 for sales, 2 for support.")
response.redirect("/voice") # Loop if no input
return str(response)
@app.route("/menu-choice", methods=["POST"])
def menu_choice():
digit = request.form.get("Digits")
response = VoiceResponse()
if digit == "1":
response.dial("+15551234567")
elif digit == "2":
response.say("Connecting to support.")
response.dial("+15559876543")
else:
response.say("Invalid option.")
response.redirect("/voice")
return str(response)
Node.js (Express)
const { VoiceResponse } = require("twilio").twiml;
app.post("/voice", (req, res) => {
const response = new VoiceResponse();
const gather = response.gather({ numDigits: 1, action: "/menu-choice" });
gather.say("Welcome. Press 1 for sales, 2 for support.");
response.redirect("/voice");
res.type("text/xml").send(response.toString());
});
app.post("/menu-choice", (req, res) => {
const digit = req.body.Digits;
const response = new VoiceResponse();
if (digit === "1") response.dial("+15551234567");
else response.say("Invalid option.").redirect("/voice");
res.type("text/xml").send(response.toString());
});
Python
from twilio.twiml.voice_response import VoiceResponse
response = VoiceResponse()
response.say("Your appointment is confirmed.", voice="alice", language="en-US")
Node.js
const { VoiceResponse } = require("twilio").twiml;
const response = new VoiceResponse();
response.say({ voice: "alice", language: "en-US" }, "Your appointment is confirmed.");
Voices: alice (default), man, woman, or Polly/Google TTS (e.g. Polly.Joanna).
Python
response = VoiceResponse()
gather = response.gather(num_digits=1, action="/handle-input", method="POST")
gather.say("Press 1 for sales, press 2 for support.")
response.say("We did not receive your input.") # Fallback if no input
Node.js
const gather = response.gather({ numDigits: 1, action: "/handle-input", method: "POST" });
gather.say("Press 1 for sales, press 2 for support.");
response.say("We did not receive your input.");
Twilio POSTs collected digits to action as Digits parameter.
Python
response = VoiceResponse()
response.play("https://example.com/audio/greeting.mp3")
Node.js
const response = new VoiceResponse();
response.play("https://example.com/audio/greeting.mp3");
Supported formats: MP3, WAV. URL must be publicly accessible.
Python
from twilio.twiml.voice_response import Dial
response = VoiceResponse()
dial = Dial(action="/dial-complete")
dial.number("+15558675310")
response.append(dial)
Node.js
const dial = response.dial({ action: "/dial-complete" });
dial.number("+15558675310");
Python
response = VoiceResponse()
response.say("Leave a message after the beep.")
response.record(
action="/recording-complete",
max_length=60,
transcribe=True,
transcribe_callback="/transcription-ready"
)
Node.js
const response = new VoiceResponse();
response.say("Leave a message after the beep.");
response.record({
action: "/recording-complete",
maxLength: 60,
transcribe: true,
transcribeCallback: "/transcription-ready",
});
Use <Dial> with action URL + <Record> in the action handler. When the dial times out or the callee is busy, the action URL serves TwiML with <Record>.
Python
# Primary TwiML — try to connect the call
response = VoiceResponse()
dial = Dial(action="/voicemail", timeout=20) # 20 seconds before voicemail
dial.number("+15558675310")
response.append(dial)
# /voicemail handler — plays if no answer
def voicemail_handler(request):
response = VoiceResponse()
response.say("We missed your call. Please leave a message after the beep.")
response.record(
action="/recording-complete",
max_length=120,
transcribe=True,
transcribe_callback="/transcription-ready",
play_beep=True
)
response.say("We didn't receive a recording. Goodbye.")
return str(response)
Node.js
// Primary TwiML — try to connect the call
const response = new VoiceResponse();
const dial = response.dial({ action: "/voicemail", timeout: 20 });
dial.number("+15558675310");
// /voicemail handler — plays if no answer
app.post("/voicemail", (req, res) => {
const response = new VoiceResponse();
response.say("We missed your call. Please leave a message after the beep.");
response.record({
action: "/recording-complete",
maxLength: 120,
transcribe: true,
transcribeCallback: "/transcription-ready",
playBeep: true,
});
response.say("We didn't receive a recording. Goodbye.");
res.type("text/xml").send(response.toString());
});
Important: <Record> captures the caller only (voicemail-style). It is NOT for recording two-party calls — see twilio-call-recordings for that.
Python
response = VoiceResponse()
dial = response.dial()
dial.conference(
"Daily Standup",
start_conference_on_enter=True,
end_conference_on_exit=True
)
Node.js
const response = new VoiceResponse();
const dial = response.dial();
dial.conference("Daily Standup", {
startConferenceOnEnter: true,
endConferenceOnExit: true,
});
Critical warnings:
- Pay Connectors are Console-only — there is no REST API to create or manage connectors. Set up in Console > Voice > Pay Connectors before coding.
- PCI Mode is IRREVERSIBLE once enabled on an account. Use a dedicated sub-account for payment calls.
Python
response = VoiceResponse()
response.say("We'll now collect your payment.")
pay = Pay(
payment_connector="stripe_connector", # Name from Console setup
charge_amount="49.99",
currency="usd",
action="/payment-complete",
status_callback="/payment-status"
)
response.append(pay)
Node.js
const response = new VoiceResponse();
response.say("We'll now collect your payment.");
response.pay({
paymentConnector: "stripe_connector",
chargeAmount: "49.99",
currency: "usd",
action: "/payment-complete",
statusCallback: "/payment-status",
});
Supported processors: Stripe, Braintree, CardConnect. Card data routes directly to the processor — never touches your server.
For production, do NOT use ngrok. Deploy your TwiML server with HTTPS:
Content-Type: text/xmlEach webhook request is stateless. To maintain conversation state across interactions:
action URLs — /next-step?language=es&dept=salesCallSidstatusCallback on the call or number config)| Parameter | Description |
|---|---|
CallSid | Unique call identifier |
From | Caller's number |
To | Called number |
CallStatus | Current status |
Direction | inbound or outbound-api |
Content-Type: text/xml<Say> verb — Split longer text across multiple <Say> elements<Record> for two-party call recording — <Record> captures the caller only (voicemail-style). For dual-channel recording of both parties, use record=True on calls.create() or the Recordings API.twilio-voice-outbound-callstwilio-voice-conversation-relaynpx claudepluginhub twilio/ai --plugin twilio-developer-kitGuides correct use of Twilio's recording methods: Record vs Dial record, dual-channel, PCI pause, Conference recording, ConversationRelay. For capturing call audio for compliance, QA, or analytics.
Provides curl examples for Telnyx Voice Media API to play audio files, use TTS, and record calls. Useful for building IVR systems, announcements, or conversation recording.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.