skills/twilio/twilio/SKILL.md
Twilio root: account management, API keys, sub-accounts, console, billing, rate limits, error codes
npx skillsauth add alphaonedev/openclaw-graph twilioInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Enable OpenClaw to operate Twilio “root” production workflows end-to-end: account and subaccount management, API keys and auth, console/billing/rate limits, and the operational patterns that sit on top (Messaging/Voice/Verify/SendGrid/Studio). This skill is for engineers who need to:
twilio 4.23.0twilio 9.0.5@sendgrid/mail 8.1.1sendgrid 6.11.0Twilio supports:
AC...)SK...) + API Key Secret (preferred over Auth Token for apps/CI)Minimum recommended production posture:
Twilio CLI is useful for interactive operations; for automation prefer REST + IaC, but CLI is still valuable for incident response.
twilio-cli 5.17.0@twilio-labs/plugin-serverless 3.0.2 (for Twilio Functions/Assets)@twilio-labs/plugin-flex 6.0.6 (if using Flex)Install via npm (see Installation & Setup).
AccountSid (AC...).Production pattern:
prod, staging, dev.MG...) which selects an appropriate sender (pooling, geo-match, sticky sender)queued, sent, delivered, undelivered, failed (and sometimes read for channels that support it, e.g., WhatsApp)<Dial>, <Conference>, <Record>, <Say> (with Polly voices), <Gather> for IVR.VA...) defines channel configuration and policies.429 and Verify-specific error codes.20429 and HTTP 429.Repository: https://github.com/twilio/twilio-python
PyPI: https://pypi.org/project/twilio/ · Supported: Python 3.7–3.13
pip install twilio
from twilio.rest import Client
import os
# Environment variables (recommended)
client = Client() # reads TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN
# API Key auth (preferred for production)
client = Client(
os.environ["TWILIO_API_KEY"],
os.environ["TWILIO_API_SECRET"],
os.environ["TWILIO_ACCOUNT_SID"]
)
# Regional edge routing
client = Client(region='au1', edge='sydney')
Source: twilio/twilio-python — client auth
Install dependencies:
sudo apt-get update
sudo apt-get install -y curl jq ca-certificates gnupg lsb-release openssl
Node.js 20.11.1 via NodeSource:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # expect v20.11.x
npm -v
Twilio CLI 5.17.0:
sudo npm install -g [email protected]
twilio --version
Optional plugins:
twilio plugins:install @twilio-labs/[email protected]
twilio plugins:install @twilio-labs/[email protected]
twilio plugins
Python 3.11 (if needed):
sudo apt-get install -y python3 python3-venv python3-pip
python3 --version
sudo dnf install -y curl jq openssl nodejs npm python3 python3-pip
node -v
sudo npm install -g [email protected]
twilio --version
Homebrew:
brew update
brew install node@20 jq openssl@3 [email protected]
brew link --force --overwrite node@20
node -v
Twilio CLI:
npm install -g [email protected]
twilio --version
Same as Intel; ensure correct PATH:
brew install node@20 jq openssl@3 [email protected]
echo 'export PATH="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
node -v
npm install -g [email protected]
twilio --version
Interactive login (stores token in local config):
twilio login
Non-interactive (CI) using env vars (preferred):
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_API_KEY="YOUR_API_KEY_SID"
export TWILIO_API_SECRET="your_api_key_secret_here"
If you must use Auth Token (not recommended for CI):
export TWILIO_AUTH_TOKEN="your_auth_token_here"
Verify auth:
twilio api:core:accounts:fetch --sid "$TWILIO_ACCOUNT_SID"
mkdir -p twilio-app && cd twilio-app
npm init -y
npm install [email protected]
npm install @sendgrid/[email protected]
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install twilio==9.0.5 sendgrid==6.11.0
From number or Messaging Service (MessagingServiceSid).<Gather> and server-side session state.Notes:
- Twilio CLI command groups can vary slightly by CLI version and installed plugins. The commands below are validated against
[email protected]with default plugins.- For automation, prefer direct REST calls; CLI is best for interactive ops.
Applies to most twilio ... commands:
-h, --help: show help-v, --version: show CLI version-l, --log-level <level>: debug|info|warn|error-o, --output <format>: json|tsv (varies by command)--profile <name>: use a named profile from ~/.twilio-cli/config.jsonExamples:
twilio -l debug api:core:accounts:fetch --sid "$TWILIO_ACCOUNT_SID"
twilio --profile prod api:core:messages:list --limit 20 -o json
Login:
twilio login
List profiles:
twilio profiles:list
Use a profile:
twilio --profile prod api:core:accounts:fetch --sid YOUR_ACCOUNT_SID
Fetch account:
twilio api:core:accounts:fetch --sid YOUR_ACCOUNT_SID
List accounts (includes subaccounts):
twilio api:core:accounts:list --limit 50
Flags:
--limit <n>: max records to return--page-size <n>: page size for API pagination--friendly-name <name>: filter by friendly name (where supported)Create subaccount:
twilio api:core:accounts:create --friendly-name "prod-messaging"
Update account status (close subaccount):
twilio api:core:accounts:update --sid YOUR_ACCOUNT_SID --status closed
Flags:
--friendly-name <name>--status <status>: active|suspended|closedList API keys:
twilio api:core:keys:list --limit 50
Create API key:
twilio api:core:keys:create --friendly-name "ci-prod-2026-02" --key-type standard
Flags:
--friendly-name <name>--key-type <type>: standard|restricted (restricted requires additional configuration; prefer standard unless you have a clear policy model)Fetch key:
twilio api:core:keys:fetch --sid YOUR_API_KEY_SID
Delete key (revoke):
twilio api:core:keys:remove --sid YOUR_API_KEY_SID
Send SMS via From:
twilio api:core:messages:create \
--from "+14155550100" \
--to "+14155550199" \
--body "prod smoke test 2026-02-21T18:42Z" \
--status-callback "https://api.example.com/twilio/sms/status"
Send via Messaging Service:
twilio api:core:messages:create \
--messaging-service-sid YOUR_MG_SID \
--to "+14155550199" \
--body "geo-match send via MG"
Important flags:
--from <E.164>: sender number--to <E.164>: recipient--body <text>--media-url <url>: repeatable for MMS--messaging-service-sid <MG...>: use messaging service instead of --from--status-callback <url>: message status webhook--max-price <decimal>: cap price (channel-dependent)--provide-feedback <boolean>: request delivery feedback (where supported)--validity-period <seconds>: TTL for message--force-delivery <boolean>: attempt to force delivery (limited applicability)--application-sid <AP...>: messaging application (legacy patterns)--smart-encoded <boolean>: enable smart encodingList messages:
twilio api:core:messages:list --limit 20
Filter list:
twilio api:core:messages:list --to "+14155550199" --date-sent 2026-02-21 --limit 50
Fetch message:
twilio api:core:messages:fetch --sid SM0123456789abcdef0123456789abcdef
List numbers:
twilio api:core:incoming-phone-numbers:list --limit 50
Purchase a number (availability varies):
twilio api:core:available-phone-numbers:us:local:list --area-code 415 --limit 5
twilio api:core:incoming-phone-numbers:create --phone-number "+14155550100" --friendly-name "prod-sms-415-0100"
Configure webhook URLs on a number:
twilio api:core:incoming-phone-numbers:update \
--sid PN0123456789abcdef0123456789abcdef \
--sms-url "https://api.example.com/twilio/sms/inbound" \
--sms-method POST \
--voice-url "https://api.example.com/twilio/voice/inbound" \
--voice-method POST
Flags (commonly used):
--sms-url <url>, --sms-method <GET|POST>--voice-url <url>, --voice-method <GET|POST>--status-callback <url> (voice call status callback for the number, depending on resource)--friendly-name <name>Create outbound call:
twilio api:core:calls:create \
--from "+14155550100" \
--to "+14155550199" \
--url "https://api.example.com/twilio/voice/twiml/outbound"
Flags:
--from, --to--url <twiml-webhook-url>: TwiML instructions endpoint--method <GET|POST>--status-callback <url>--status-callback-event <initiated|ringing|answered|completed> (repeatable)--status-callback-method <GET|POST>--timeout <seconds>--record <boolean>--recording-status-callback <url>--recording-status-callback-method <GET|POST>Fetch call:
twilio api:core:calls:fetch --sid YOUR_CA_SID
List calls:
twilio api:core:calls:list --from "+14155550100" --start-time 2026-02-21 --limit 50
Verify is often easiest via REST calls. Example with curl:
Send verification:
curl -sS -X POST "https://verify.twilio.com/v2/Services/YOUR_VERIFY_SERVICE_SID/Verifications" \
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
--data-urlencode "To=+14155550199" \
--data-urlencode "Channel=sms"
Check verification:
curl -sS -X POST "https://verify.twilio.com/v2/Services/YOUR_VERIFY_SERVICE_SID/VerificationCheck" \
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
--data-urlencode "To=+14155550199" \
--data-urlencode "Code=123456"
Trigger a Studio Flow execution:
curl -sS -X POST "https://studio.twilio.com/v2/Flows/FW0123456789abcdef0123456789abcdef/Executions" \
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
--data-urlencode "To=+14155550199" \
--data-urlencode "From=+14155550100" \
--data-urlencode "Parameters={\"experiment\":\"B\",\"locale\":\"en-US\"}"
Initialize:
mkdir -p twilio-functions && cd twilio-functions
twilio serverless:init twilio-prod-webhooks --template blank
Deploy:
twilio serverless:deploy --environment production --force
Flags:
--environment <name>: environment in Twilio Serverless--force: skip prompts--service-name <name>: override service name--functions-folder <path>--assets-folder <path>Path:
~/.twilio-cli/config.json%USERPROFILE%\.twilio-cli\config.jsonExample ~/.twilio-cli/config.json:
{
"profiles": {
"prod": {
"accountSid": "YOUR_ACCOUNT_SID",
"apiKeySid": "YOUR_API_KEY_SID",
"apiKeySecret": "ENV:TWILIO_API_SECRET",
"region": "us1",
"edge": "ashburn"
},
"staging": {
"accountSid": "YOUR_ACCOUNT_SID",
"authToken": "ENV:TWILIO_AUTH_TOKEN_STAGING",
"region": "us1"
}
},
"activeProfile": "prod"
}
Notes:
apiKeySecret sourced from environment (ENV:...) rather than plaintext.region/edge can reduce latency; validate against your Twilio deployment.Recommended file:
/etc/twilio/twilio.env (Linux servers)Example /etc/twilio/twilio.env:
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_API_KEY=YOUR_API_KEY_SID
TWILIO_API_SECRET=supersecret_api_key_secret
TWILIO_MESSAGING_SERVICE_SID=YOUR_MG_SID
TWILIO_VERIFY_SERVICE_SID=YOUR_VERIFY_SERVICE_SID
TWILIO_WEBHOOK_AUTH_TOKEN=your_auth_token_for_signature_validation
TWILIO_REGION=us1
TWILIO_EDGE=ashburn
SENDGRID_API_KEY=SG.xxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
TWILIO_WEBHOOK_AUTH_TOKEN:
Path:
/etc/nginx/conf.d/twilio-webhooks.confExample:
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location /twilio/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-Id $request_id;
proxy_read_timeout 10s;
proxy_connect_timeout 2s;
}
}
Path:
/etc/systemd/system/twilio-webhooks.serviceExample:
[Unit]
Description=Twilio Webhook Service
After=network-online.target
[Service]
User=twilio
Group=twilio
EnvironmentFile=/etc/twilio/twilio.env
WorkingDirectory=/opt/twilio-webhooks
ExecStart=/usr/bin/node /opt/twilio-webhooks/server.js
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/twilio-webhooks /var/log/twilio-webhooks
[Install]
WantedBy=multi-user.target
Pattern:
TWILIO_API_SECRET, TWILIO_WEBHOOK_AUTH_TOKEN, SENDGRID_API_KEY in a secret manager.Example: Kubernetes External Secrets (AWS Secrets Manager) snippet:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: twilio-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: ClusterSecretStore
target:
name: twilio-secrets
data:
- secretKey: TWILIO_API_SECRET
remoteRef:
key: prod/twilio
property: api_secret
- secretKey: TWILIO_WEBHOOK_AUTH_TOKEN
remoteRef:
key: prod/twilio
property: auth_token
- secretKey: SENDGRID_API_KEY
remoteRef:
key: prod/sendgrid
property: api_key
Pipeline steps:
Smoke test example (Node):
node scripts/smoke_sms.js
scripts/smoke_sms.js:
import twilio from "twilio";
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const apiKey = process.env.TWILIO_API_KEY;
const apiSecret = process.env.TWILIO_API_SECRET;
const mg = process.env.TWILIO_MESSAGING_SERVICE_SID;
const client = twilio(apiKey, apiSecret, { accountSid });
const to = "+14155550199";
const msg = await client.messages.create({
messagingServiceSid: mg,
to,
body: `smoke ${new Date().toISOString()}`
});
console.log({ sid: msg.sid, status: msg.status });
Pattern:
/twilio/sms/status.MessageSid.Benefits:
Pattern:
<Gather> with action pointing to your app.CallSid.Pattern:
experiment parameter in your DB.Symptom (Twilio API response):
21211The 'To' number +1415555 is not a valid phone number.Root causes:
Fix:
+14155550199).whatsapp:+E164.Symptom:
20003AuthenticateOr HTTP 401 with:
HTTP 401 UnauthorizedRoot causes:
accountSid in SDK client options).Fix:
twilio(apiKey, apiSecret, { accountSid }).Symptom:
20429Too Many RequestsOr HTTP 429.
Root causes:
Fix:
Symptom:
30003Unreachable destination handsetRoot causes:
Fix:
Symptom in your logs:
Error: Twilio Request Validation Failed. Expected signature ...Root causes:
Fix:
Symptoms:
Root causes:
StatusCallback not set on message or messaging service.Fix:
statusCallback per message or configure at Messaging Service level.200 quickly (< 5s) and processes async.Symptom:
11200 HTTP retrieval failure11205 HTTP retrieval failureRoot causes:
Fix:
Content-Type: text/xml.Common symptoms:
Root causes:
Fix:
Symptom:
HTTP Error 403: Forbiddenpermission denied, wrong scopesRoot causes:
Mail Send permission.Fix:
Mail Send scope.Symptom:
Root causes:
FW...) or wrong account.Fix:
MessageSidCallSid + event type200 quickly; enqueue work.NoNewPrivileges=true, ProtectSystem=strict, etc.).Expected impact (typical):
Expected impact:
20429 / HTTP 429.Expected impact:
Expected impact:
11200/11205 retrieval failures; faster call connect.server.js (Express):
import express from "express";
import twilio from "twilio";
import crypto from "crypto";
const app = express();
// Twilio sends application/x-www-form-urlencoded by default
app.use(express.urlencoded({ extended: false }));
const {
TWILIO_ACCOUNT_SID,
TWILIO_API_KEY,
TWILIO_API_SECRET,
TWILIO_MESSAGING_SERVICE_SID,
TWILIO_WEBHOOK_AUTH_TOKEN
} = process.env;
const client = twilio(TWILIO_API_KEY, TWILIO_API_SECRET, { accountSid: TWILIO_ACCOUNT_SID });
function validateTwilioSignature(req) {
const signature = req.header("X-Twilio-Signature");
const url = `https://${req.get("host")}${req.originalUrl}`;
const params = req.body;
const isValid = twilio.validateRequest(TWILIO_WEBHOOK_AUTH_TOKEN, signature, url, params);
return isValid;
}
// naive in-memory dedupe for example; use Redis/DB in production
const seen = new Set();
app.post("/twilio/sms/status", (req, res) => {
if (!validateTwilioSignature(req)) return res.status(403).send("invalid signature");
const messageSid = req.body.MessageSid;
const messageStatus = req.body.MessageStatus;
const key = `${messageSid}:${messageStatus}`;
if (seen.has(key)) return res.status(200).send("duplicate");
seen.add(key);
// enqueue to your queue here
console.log({ messageSid, messageStatus, errorCode: req.body.ErrorCode, errorMessage: req.body.ErrorMessage });
res.status(200).send("ok");
});
app.post("/send", async (req, res) => {
const to = req.body.to;
const body = req.body.body;
const msg = await client.messages.create({
messagingServiceSid: TWILIO_MESSAGING_SERVICE_SID,
to,
body,
statusCallback: "https://api.example.com/twilio/sms/status"
});
res.json({ sid: msg.sid, status: msg.status });
});
app.listen(8080, () => console.log("listening on :8080"));
Run:
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_API_KEY="YOUR_API_KEY_SID"
export TWILIO_API_SECRET="supersecret_api_key_secret"
export TWILIO_MESSAGING_SERVICE_SID="YOUR_MG_SID"
export TWILIO_WEBHOOK_AUTH_TOKEN="your_auth_token_for_signature_validation"
node server.js
curl -sS -X POST http://localhost:8080/send -d 'to=+14155550199' -d 'body=hello from prod'
Key points:
Body case-insensitively and trim.from flask import Flask, request, abort
from twilio.request_validator import RequestValidator
import os
app = Flask(__name__)
validator = RequestValidator(os.environ["TWILIO_WEBHOOK_AUTH_TOKEN"])
suppressed = set() # replace with DB table keyed by E.164
def valid(req):
signature = req.headers.get("X-Twilio-Signature", "")
url = "https://api.example.com" + req.path
return validator.validate(url, req.form, signature)
@app.post("/twilio/sms/inbound")
def inbound_sms():
if not valid(request):
abort(403)
from_ = request.form.get("From", "")
body = (request.form.get("Body", "") or "").strip().upper()
if body in {"STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"}:
suppressed.add(from_)
return ("", 200)
if body in {"START", "YES", "UNSTOP"}:
suppressed.discard(from_)
return ("", 200)
# normal inbound processing
return ("", 200)
Inbound webhook returns TwiML:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="Polly.Joanna">Welcome. Press 1 for sales. Press 2 for support.</Say>
<Gather numDigits="1" action="/twilio/voice/menu" method="POST" timeout="5">
<Say voice="Polly.Joanna">Make your selection now.</Say>
</Gather>
<Say voice="Polly.Joanna">No input received. Goodbye.</Say>
<Hangup/>
</Response>
Menu handler returns TwiML based on digit:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial>
<Queue>support</Queue>
</Dial>
</Response>
Production gotchas:
action if behind proxies that rewrite paths.CallSid state if multi-step.Pseudo-flow:
429 or repeated failures, offer voice call or email.Send SMS verify (curl shown earlier). Handle 429:
resp="$(curl -sS -w "\n%{http_code}" -X POST \
"https://verify.twilio.com/v2/Services/$TWILIO_VERIFY_SERVICE_SID/Verifications" \
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
--data-urlencode "To=+14155550199" \
--data-urlencode "Channel=sms")"
body="$(echo "$resp" | head -n1)"
code="$(echo "$resp" | tail -n1)"
if [ "$code" = "429" ]; then
echo "rate limited; offer voice/email fallback"
exit 0
fi
echo "$body" | jq .
import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: "[email protected]",
from: "[email protected]",
templateId: "d-13b8f94f2f2c4c0f9a2d8b2d3b7a9c01",
dynamicTemplateData: {
incident_id: "INC-2026-021",
service: "messaging-api",
severity: "SEV2",
started_at: "2026-02-21T18:42:00Z"
}
};
const [resp] = await sgMail.send(msg);
console.log(resp.statusCode);
Operational notes:
Trigger with parameter experiment:
curl -sS -X POST "https://studio.twilio.com/v2/Flows/FW0123456789abcdef0123456789abcdef/Executions" \
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
--data-urlencode "To=+14155550199" \
--data-urlencode "From=+14155550100" \
--data-urlencode "Parameters={\"experiment\":\"A\",\"user_id\":\"u_9f2c1\"}"
In Studio:
{{flow.data.experiment}}.| Task | Command / API | Key flags / fields |
|---|---|---|
| Login CLI | twilio login | n/a |
| Fetch account | twilio api:core:accounts:fetch --sid AC... | --sid |
| Create subaccount | twilio api:core:accounts:create | --friendly-name |
| Close subaccount | twilio api:core:accounts:update | --sid, --status closed |
| List API keys | twilio api:core:keys:list | --limit |
| Create API key | twilio api:core:keys:create | --friendly-name, --key-type |
| Revoke API key | twilio api:core:keys:remove | --sid |
| Send SMS (From) | twilio api:core:messages:create | --from, --to, --body, --status-callback |
| Send SMS (MG) | twilio api:core:messages:create | --messaging-service-sid, --to, --body |
| Fetch message | twilio api:core:messages:fetch | --sid SM... |
| Configure number webhooks | twilio api:core:incoming-phone-numbers:update | --sms-url, --voice-url, methods |
| Create outbound call | twilio api:core:calls:create | --from, --to, --url, callbacks |
| Verify send | POST /v2/Services/{VA}/Verifications | To, Channel |
| Verify check | POST /v2/Services/{VA}/VerificationCheck | To, Code |
| Trigger Studio | POST /v2/Flows/{FW}/Executions | To, From, Parameters |
DEPENDS_ON
http (webhooks, REST APIs)tls (HTTPS endpoints, certificate validation)secrets-management (Vault/KMS/Secrets Manager)dns (webhook reachability)queueing (Kafka/SQS/PubSub for webhook processing)COMPOSES
kubernetes (deploy webhook services, manage secrets, autoscaling)terraform (manage Twilio resources where feasible; otherwise manage config + secrets)observability (structured logs, tracing, alerting on error codes like 20429/30003)incident-response (runbooks for auth rotation, webhook failures, carrier incidents)SIMILAR_TO
nexmo-vonage (messaging/voice APIs, webhooks, compliance)plivo (programmable communications)aws-sns (messaging primitives; different compliance and feature set)sendgrid (email delivery; overlaps with Twilio SendGrid component)tools
Root web development: project structure, tooling selection, deployment decisions
development
WebAssembly: Rust/Go/C to WASM, wasm-bindgen, Emscripten, WASM Component Model
development
Vue 3: Composition API script setup, Pinia, Vue Router 4, SFCs, Vite, Nuxt 3
tools
Tailwind CSS 4: utility classes, config, JIT, arbitrary values, darkMode, plugins, shadcn/ui