skills/twilio/twilio-video/SKILL.md
Video rooms: group/P2P, recording composition, track publication, network quality API, bandwidth
npx skillsauth add alphaonedev/openclaw-graph twilio-videoInstall 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 design, implement, operate, and troubleshoot Twilio Video in production:
This guide assumes you are building a multi-tenant, production WebRTC system with compliance, observability, and incident response requirements.
AC...)SK...) and Secret...) for legacy/basic auth; prefer API Key + Secret for server-to-server.Create API Key:
twilio api:core:keys:create --friendly-name "video-prod-key-2026-02"
Server-side (token minting, REST API calls):
node >= 20.11.1 (LTS), npm >= 10.2.4
[email protected] (Twilio Node helper library)[email protected] (if you implement custom JWT handling; Twilio helper can mint tokens)python >= 3.11.7
twilio==9.4.1go >= 1.22.1
github.com/twilio/[email protected]Client-side:
[email protected]
5.10.0 (CocoaPods/SPM)7.6.0 (Gradle)Twilio CLI:
[email protected] (Node-based)@twilio-labs/[email protected] (Video commands; plugin availability varies—verify in your environment)Install Twilio CLI:
npm i -g [email protected]
twilio --version
Install plugin:
twilio plugins:install @twilio-labs/[email protected]
twilio plugins
Prefer environment variables:
TWILIO_ACCOUNT_SIDTWILIO_API_KEYTWILIO_API_SECRETTWILIO_AUTH_TOKEN (avoid in CI if possible)Example (bash):
export TWILIO_ACCOUNT_SID="YOUR_ACCOUNT_SID"
export TWILIO_API_KEY="YOUR_API_KEY_SID"
export TWILIO_API_SECRET="your_api_secret_from_console"
Twilio CLI login (stores credentials locally; not recommended for CI runners):
twilio login
Key properties:
type: group | group-small | peer-to-peer (availability depends on account/region)status: in-progress | completedmaxParticipants: enforce caps to control cost and qualitypublished → subscribed (remote) → unpublished / stoppedTwilio Video uses JWT access tokens with a VideoGrant:
identity: stable user identifier (tenant-scoped)ttl: seconds; keep short (e.g., 3600) and refreshroom: optional; restrict token to a specific roomClock skew is a common failure mode; keep NTP.
Twilio Video “recording” in production typically means Compositions:
low/standard/high) influences which tracks degrade first.Common callback types:
Webhooks must be:
Repository: https://github.com/twilio/twilio-python
PyPI: pip install twilio · Supported: Python 3.7–3.13
from twilio.rest import Client
from twilio.jwt.access_token import AccessToken
from twilio.jwt.access_token.grants import VideoGrant
client = Client()
# Create a room
room = client.video.v1.rooms.create(
unique_name="DailyStandup",
type="go" # or 'group' / 'peer-to-peer'
)
print(room.sid)
# Generate participant access token
token = AccessToken(
os.environ["TWILIO_ACCOUNT_SID"],
os.environ["TWILIO_API_KEY"],
os.environ["TWILIO_API_SECRET"],
identity="alice"
)
token.add_grant(VideoGrant(room="DailyStandup"))
print(token.to_jwt())
Source: twilio/twilio-python — video
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
npm -v
sudo npm i -g [email protected]
twilio --version
twilio plugins:install @twilio-labs/[email protected]
sudo dnf install -y nodejs npm jq
node -v
npm -v
sudo npm i -g [email protected]
twilio plugins:install @twilio-labs/[email protected]
Using Homebrew:
brew install node@20 jq
echo '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
npm i -g [email protected]
twilio plugins:install @twilio-labs/[email protected]
Install Node.js 20 LTS from official installer or winget:
winget install OpenJS.NodeJS.LTS
node -v
npm -v
npm i -g [email protected]
twilio --version
twilio plugins:install @twilio-labs/[email protected]
mkdir -p video-service && cd video-service
npm init -y
npm i [email protected] [email protected] @fastify/[email protected] [email protected]
npm i -D [email protected] [email protected] @types/[email protected]
Minimal tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip==24.0
pip install twilio==9.4.1 fastapi==0.109.2 uvicorn==0.27.1 python-dotenv==1.0.1
Production patterns:
uniqueName naming conventions: include tenant + resource id.maxParticipants to control cost and prevent abuse.Example naming scheme:
acme-prod:class:9f3c2b1atenantId:resourceType:resourceIdNode: create room:
import twilio from "twilio";
const client = twilio(process.env.TWILIO_API_KEY!, process.env.TWILIO_API_SECRET!, {
accountSid: process.env.TWILIO_ACCOUNT_SID!,
});
async function createRoom() {
const room = await client.video.v1.rooms.create({
uniqueName: "acme-prod:class:9f3c2b1a",
type: "group",
maxParticipants: 25,
recordParticipantsOnConnect: false
});
return room;
}
Complete room:
await client.video.v1.rooms("RM0123456789abcdef0123456789abcdef")
.update({ status: "completed" });
Token service requirements:
Node token minting:
import twilio from "twilio";
const { AccessToken } = twilio.jwt;
const { VideoGrant } = AccessToken;
export function mintVideoToken(params: {
identity: string;
room?: string;
ttlSeconds?: number;
}) {
const token = new AccessToken(
process.env.TWILIO_ACCOUNT_SID!,
process.env.TWILIO_API_KEY!,
process.env.TWILIO_API_SECRET!,
{
identity: params.identity,
ttl: params.ttlSeconds ?? 3600
}
);
token.addGrant(new VideoGrant({ room: params.room }));
return token.toJwt();
}
Python token minting:
from twilio.jwt.access_token import AccessToken
from twilio.jwt.access_token.grants import VideoGrant
def mint_video_token(identity: str, room: str | None = None, ttl: int = 3600) -> str:
token = AccessToken(
account_sid=os.environ["TWILIO_ACCOUNT_SID"],
signing_key_sid=os.environ["TWILIO_API_KEY"],
secret=os.environ["TWILIO_API_SECRET"],
identity=identity,
ttl=ttl,
)
token.add_grant(VideoGrant(room=room))
return token.to_jwt()
Client-side (JS) connect with bandwidth profile and dominant speaker:
import Video from "twilio-video";
const room = await Video.connect(token, {
name: "acme-prod:class:9f3c2b1a",
dominantSpeaker: true,
networkQuality: { local: 1, remote: 1 },
bandwidthProfile: {
video: {
mode: "collaboration",
dominantSpeakerPriority: "high",
maxSubscriptionBitrate: 2500000,
renderDimensions: {
high: { width: 1280, height: 720 },
standard: { width: 640, height: 360 },
low: { width: 320, height: 180 }
}
}
}
});
Moderation patterns:
await client.video.v1.rooms("RM...").participants("PA...")
.update({ status: "disconnected" });
Typical flow:
processing → completed/failed).Create composition (Node):
const composition = await client.video.v1.compositions.create({
roomSid: "RM0123456789abcdef0123456789abcdef",
audioSources: "*",
videoLayout: {
grid: { video_sources: ["*"] }
},
format: "mp4",
statusCallback: "https://video.acme.com/twilio/composition-status",
statusCallbackMethod: "POST"
});
Fetch composition media:
const c = await client.video.v1.compositions("CJ0123456789abcdef0123456789abcdef").fetch();
console.log(c.links?.media);
Client-side event handling:
room.on("networkQualityLevelChanged", (level, stats) => {
// level: 0..5
// stats: { audio: { send, recv }, video: { send, recv } } depending on SDK
if (level <= 2) {
console.warn("Network degraded", level, stats);
}
});
Server-side: fetch participant network quality (where supported by REST API fields; otherwise rely on client telemetry).
Production defaults:
maxSubscriptionBitrate to cap downstream usage.const videoTrack = Array.from(room.localParticipant.videoTracks.values())[0]?.track;
videoTrack?.setPriority("high");
Requirements:
X-Twilio-Signature)CompositionSid + StatusFastify example:
import Fastify from "fastify";
import twilio from "twilio";
const app = Fastify({ logger: true });
app.post("/twilio/composition-status", async (req, reply) => {
const signature = req.headers["x-twilio-signature"] as string | undefined;
const url = "https://video.acme.com/twilio/composition-status";
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!, // signature validation uses Auth Token
signature ?? "",
url,
req.body as Record<string, string>
);
if (!isValid) return reply.code(403).send({ error: "invalid signature" });
// process CompositionSid, Status, RoomSid, etc.
return reply.code(204).send();
});
app.listen({ port: 3000, host: "0.0.0.0" });
Note: Signature validation uses Auth Token, not API Secret. If you avoid Auth Token in services, isolate webhook verification into a small component with restricted secret access.
Notes:
- Twilio CLI command availability depends on installed plugins and Twilio CLI version.
- For Video, many operations are easiest via REST API using helper libraries; CLI is useful for inspection.
Common across commands:
-o, --output [json|tsv|table] output format--properties <props> comma-separated properties to display--no-header omit header (tsv/table)--profile <name> use a named profile from ~/.twilio-cli/config.json--log-level [debug|info|warn|error]Example:
twilio api:video:v1:rooms:list -o json --log-level debug
List rooms:
twilio api:video:v1:rooms:list \
--status in-progress \
--limit 50 \
-o table \
--properties sid,uniqueName,status,type,maxParticipants,dateCreated
Relevant flags:
--status <in-progress|completed>--unique-name <string> (filter; if supported by CLI generator)--limit <int>Fetch room:
twilio api:video:v1:rooms:fetch --sid RM0123456789abcdef0123456789abcdef -o json
Create room:
twilio api:video:v1:rooms:create \
--unique-name "acme-prod:class:9f3c2b1a" \
--type group \
--max-participants 25 \
--record-participants-on-connect false \
-o json
Update room (complete):
twilio api:video:v1:rooms:update \
--sid RM0123456789abcdef0123456789abcdef \
--status completed \
-o json
List participants in a room:
twilio api:video:v1:rooms:participants:list \
--room-sid RM0123456789abcdef0123456789abcdef \
--status connected \
--limit 200 \
-o table \
--properties sid,identity,status,dateCreated
Flags:
--room-sid <RM...>--status <connected|disconnected>--identity <string> (if supported)--limit <int>Disconnect participant:
twilio api:video:v1:rooms:participants:update \
--room-sid RM0123456789abcdef0123456789abcdef \
--sid PA0123456789abcdef0123456789abcdef \
--status disconnected \
-o json
Create composition:
twilio api:video:v1:compositions:create \
--room-sid RM0123456789abcdef0123456789abcdef \
--audio-sources "*" \
--format mp4 \
--status-callback "https://video.acme.com/twilio/composition-status" \
--status-callback-method POST \
-o json
Fetch composition:
twilio api:video:v1:compositions:fetch \
--sid CJ0123456789abcdef0123456789abcdef \
-o json
List compositions:
twilio api:video:v1:compositions:list \
--room-sid RM0123456789abcdef0123456789abcdef \
--limit 20 \
-o table \
--properties sid,status,format,dateCreated
List recordings for a room:
twilio api:video:v1:recordings:list \
--grouping-sid RM0123456789abcdef0123456789abcdef \
--limit 50 \
-o table \
--properties sid,status,trackName,dateCreated
Create key:
twilio api:core:keys:create --friendly-name "video-key-rotation-2026-02" -o json
List keys:
twilio api:core:keys:list -o table --properties sid,friendlyName,dateCreated
Revoke key:
twilio api:core:keys:remove --sid YOUR_API_KEY_SID
Path (Linux systemd pattern):
/etc/openclaw/video-service.envExample:
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_API_KEY=YOUR_API_KEY_SID
TWILIO_API_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
VIDEO_TOKEN_TTL_SECONDS=3600
VIDEO_ROOM_TYPE=group
VIDEO_MAX_PARTICIPANTS=25
PUBLIC_BASE_URL=https://video.acme.com
COMPOSITION_STATUS_CALLBACK_URL=https://video.acme.com/twilio/composition-status
LOG_LEVEL=info
Path:
/etc/systemd/system/openclaw-video.service[Unit]
Description=OpenClaw Twilio Video Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=/etc/openclaw/video-service.env
WorkingDirectory=/opt/openclaw/video-service
ExecStart=/usr/bin/node /opt/openclaw/video-service/dist/server.js
Restart=on-failure
RestartSec=2
User=openclaw
Group=openclaw
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/openclaw-video
AmbientCapabilities=
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.target
Path:
/etc/nginx/sites-available/video.acme.com/etc/nginx/sites-enabled/video.acme.comserver {
listen 443 ssl http2;
server_name video.acme.com;
ssl_certificate /etc/letsencrypt/live/video.acme.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/video.acme.com/privkey.pem;
client_max_body_size 2m;
location /twilio/ {
proxy_pass http://127.0.0.1:3000;
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 30s;
}
location /healthz {
proxy_pass http://127.0.0.1:3000/healthz;
}
}
Path:
~/.twilio-cli/config.jsonExample with profiles:
{
"activeProfile": "prod",
"profiles": {
"prod": {
"accountSid": "YOUR_ACCOUNT_SID",
"apiKeySid": "YOUR_API_KEY_SID",
"apiKeySecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"staging": {
"accountSid": "YOUR_ACCOUNT_SID",
"apiKeySid": "YOUR_API_KEY_SID",
"apiKeySecret": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
}
}
}
Pattern:
Pseudo-flow:
POST /verify/start → Twilio Verify V2POST /verify/checkPOST /video/token (requires verified session)Operational notes:
Use Messaging to send:
Production requirements:
Pipeline example:
completedSendGrid transactional email:
Use Studio Flow to orchestrate:
Trigger Studio via REST Trigger API from your backend when room completes.
Smoke test script:
Include these in runbooks; ensure logs capture Twilio request IDs.
Error (REST):
TwilioRestException: Authenticate
HTTP 401
{"code":20003,"message":"Authenticate","more_info":"https://www.twilio.com/docs/errors/20003","status":401}
Root causes:
Fix:
TWILIO_ACCOUNT_SID, TWILIO_API_KEY, TWILIO_API_SECRETaccountSid when using API keysError:
TwilioRestException: Too Many Requests
HTTP 429
{"code":20429,"message":"Too Many Requests","more_info":"https://www.twilio.com/docs/errors/20429","status":429}
Root causes:
Fix:
Error (typical):
TwilioRestException: The requested resource /Rooms was not found
HTTP 404
Or validation errors depending on endpoint. Another common failure is attempting to create a room with an existing uniqueName while it’s in-progress.
Root causes:
uniqueName for concurrent sessionsFix:
completedClient error (JS SDK):
TwilioError: Access Token expired or invalid
Root causes:
Fix:
Your service logs:
invalid signature
Root causes:
TWILIO_AUTH_TOKENFix:
Host and X-Forwarded-ProtoComposition status remains processing for extended time.
Root causes:
Fix:
audioSources: "*" and video_sources: ["*"] for broad captureprocessing > threshold (e.g., 2 hours), alert and open Twilio support ticket with Composition SIDClient logs show ICE failures; typical symptom: “Connecting…” then disconnect.
Root causes:
Fix:
Browser error:
NotAllowedError: Permission denied
Root causes:
Fix:
Some SDKs surface:
TwilioError: Participant identity is already in use
Root causes:
Fix:
userId:deviceId and map to user in app layerIf you send SMS notifications and see:
{"code":21211,"message":"The 'To' number +1555... is not a valid phone number.","status":400}
Fix:
TWILIO_API_SECRET and TWILIO_AUTH_TOKEN in a secrets manager:
X-Twilio-Signature.CompositionSid + Status unique constraint in DBFor Linux hosts (CIS Ubuntu 22.04 LTS guidance):
NoNewPrivileges, ProtectSystem=strict, etc.).Before: polling room participants every 2 seconds per active room.
After: event-driven + cached state:
Implementation:
Set maxSubscriptionBitrate to cap downstream:
2.5 Mbps cap for collaboration.high, others standard/low.Use 720p for presenter, 360p for others:
const localVideoTrack = await Video.createLocalVideoTrack({
width: 1280,
height: 720,
frameRate: 24
});
Expected impact:
Pattern:
ACTIVE_SIGNING_KEY_SID and NEXT_SIGNING_KEY_SID.If your “room completed” handler can run twice:
roomSid for composition job.When a network blip occurs, many clients reconnect simultaneously.
Mitigations:
Use data tracks for:
But do not rely on them for authoritative moderation; enforce server-side policy.
Steps:
Backend (Node) create room:
const room = await client.video.v1.rooms.create({
uniqueName: "acme-prod:webinar:2026-02-21T18-00Z",
type: "group",
maxParticipants: 100,
recordParticipantsOnConnect: false
});
Token endpoint returns JWT scoped to room:
const jwt = mintVideoToken({
identity: "tenant_acme:user_42",
room: "acme-prod:webinar:2026-02-21T18-00Z",
ttlSeconds: 1800
});
Admin UI calls backend:
curl -X POST https://video.acme.com/admin/rooms/RM.../participants/PA.../disconnect
Backend:
await client.video.v1.rooms(roomSid).participants(participantSid)
.update({ status: "disconnected" });
Audit log:
Flow:
completed, store media URL, send SMS.Composition callback handler:
Status=completed, enqueue job send_recording_smsSMS send (ensure STOP handling + status callbacks in your messaging service).
Client:
networkQualityLevelChanged.Pseudo:
let lowSince = null;
room.on("networkQualityLevelChanged", (level) => {
const now = Date.now();
if (level <= 1) {
lowSince ??= now;
if (now - lowSince > 10000) {
room.localParticipant.videoTracks.forEach(pub => pub.track.disable());
}
} else {
lowSince = null;
}
});
Commands:
twilio api:core:keys:create --friendly-name "video-prod-2026-03" -o json
twilio api:core:keys:list -o table --properties sid,friendlyName,dateCreated
twilio api:core:keys:remove --sid SKoldkey...
Mitigation steps:
| Task | Command / API | Key flags / fields |
|---|---|---|
| List in-progress rooms | twilio api:video:v1:rooms:list | --status in-progress --limit 50 -o table |
| Create room | twilio api:video:v1:rooms:create | --unique-name ... --type group --max-participants N |
| Complete room | twilio api:video:v1:rooms:update | --sid RM... --status completed |
| List participants | twilio api:video:v1:rooms:participants:list | --room-sid RM... --status connected |
| Disconnect participant | twilio api:video:v1:rooms:participants:update | --room-sid RM... --sid PA... --status disconnected |
| Create composition | twilio api:video:v1:compositions:create | --room-sid RM... --audio-sources "*" --format mp4 --status-callback URL |
| Fetch composition | twilio api:video:v1:compositions:fetch | --sid CJ... |
| Rotate API key | twilio api:core:keys:create/remove | --friendly-name ... / --sid SK... |
| Mint token (Node) | AccessToken + VideoGrant | identity, ttl, room |
twilio-core-auth: Account SID, API Key/Secret, Auth Token handling, key rotationtwilio-webhooks: signature validation, retry/idempotency patternswebrtc-client: browser/device constraints, ICE/TURN behavior, media permissionsobservability: structured logs, request IDs, metrics, alertingtwilio-messaging: SMS/WhatsApp notifications for session events, recording delivery, status callbacks, STOP handlingtwilio-verify: step-up authentication before token minting / recording accesssendgrid-email: transactional email for recording links, audit trails, bounce handlingtwilio-studio: orchestration of reminders, surveys, escalation flowstwilio-voice: PSTN fallback for audio when WebRTC fails; IVR for supportzoom-video-sdk (conceptually similar: rooms/sessions, tokens, recording)agora-rtc (tracks, channel join, bandwidth profiles)daily-webrtc (rooms, recording, webhooks)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