twilio-video
twilio-video
Purpose
Enable OpenClaw to design, implement, operate, and troubleshoot Twilio Video in production:
- Create and manage Video Rooms (Group and Peer-to-Peer), including lifecycle controls.
- Issue Access Tokens with Video grants (identity, room scoping, TTL, key rotation).
- Publish/subscribe tracks (audio/video/data), manage track priorities, and implement moderation (mute/unpublish/remove participants).
- Enable and operate recording via Compositions (server-side recording outputs), including callbacks and storage pipelines.
- Use Network Quality API and bandwidth profiles to maintain call quality under real-world network constraints.
- Integrate Video with Twilio cluster patterns: webhooks, auth, rate limits, cost controls, and operational playbooks.
This guide assumes you are building a multi-tenant, production WebRTC system with compliance, observability, and incident response requirements.
Prerequisites
Twilio account + credentials
- Twilio Account SID (format
AC...) - Twilio API Key SID (format
SK...) and Secret - (Optional) Twilio Auth Token (format
...) for legacy/basic auth; prefer API Key + Secret for server-to-server. - A Twilio Video-enabled project (default for most accounts).
Create API Key:
twilio api:core:keys:create --friendly-name "video-prod-key-2026-02"
Supported SDKs / libraries (pin versions)
Server-side (token minting, REST API calls):
- Node.js:
node >= 20.11.1(LTS), npm>= 10.2.4twilio@4.23.0(Twilio Node helper library)jsonwebtoken@9.0.2(if you implement custom JWT handling; Twilio helper can mint tokens)
- Python:
python >= 3.11.7twilio==9.4.1
- Go:
go >= 1.22.1github.com/twilio/twilio-go@v1.20.3
Client-side:
- Twilio Video JS SDK:
twilio-video@2.31.1- Browser support: latest Chrome/Edge, Firefox ESR, Safari 17+ (macOS/iOS constraints apply)
- iOS: TwilioVideo iOS SDK
5.10.0(CocoaPods/SPM) - Android: Twilio Video Android SDK
7.6.0(Gradle)
Twilio CLI:
twilio-cli@5.5.1(Node-based)- Plugins:
@twilio-labs/plugin-video@0.7.0(Video commands; plugin availability varies—verify in your environment)
Install Twilio CLI:
npm i -g twilio-cli@5.5.1
twilio --version
Install plugin:
twilio plugins:install @twilio-labs/plugin-video@0.7.0
twilio plugins
Auth setup (local dev + CI)
Prefer environment variables:
TWILIO_ACCOUNT_SIDTWILIO_API_KEYTWILIO_API_SECRET- (Optional)
TWILIO_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
Network + infra prerequisites
- Public HTTPS endpoint for webhooks (Composition callbacks, Status callbacks).
- TLS: modern ciphers; certificate from a trusted CA (Let’s Encrypt OK).
- Time sync: NTP enabled on servers minting JWTs (clock skew breaks tokens).
- TURN/STUN: Twilio provides; ensure outbound UDP/TCP 3478/5349 allowed; enterprise networks may require TCP/TLS fallback.
Core Concepts
Rooms: Group vs Peer-to-Peer (P2P)
- Group Room: media routed via Twilio’s SFU; supports larger rooms, recording/compositions, bandwidth profiles, network quality, dominant speaker, etc. Default for most production use.
- Peer-to-Peer Room: direct mesh between participants; limited scalability; fewer server-side features; generally not recommended for production beyond 1:1 with strict latency constraints.
Key properties:
type:group|group-small|peer-to-peer(availability depends on account/region)status:in-progress|completedmaxParticipants: enforce caps to control cost and quality
Participants and Tracks
- Participant: identity in a room (unique per room).
- Tracks:
- Audio track
- Video track
- Data track (reliable/unreliable messaging)
- Track lifecycle:
published→subscribed(remote) →unpublished/stopped - Server-side moderation often means:
- Disconnect participant
- Force unpublish (where supported)
- Enforce client behavior via signaling + policy
Access Tokens (JWT)
Twilio 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 room- Signed with API Key Secret
Clock skew is a common failure mode; keep NTP.
Recording via Compositions
Twilio Video “recording” in production typically means Compositions:
- A Composition is a server-side rendered output (e.g., MP4) created from recorded tracks.
- You can configure:
- Layout (grid, active speaker, custom)
- Format
- Status callback URL
- You must handle asynchronous completion and storage (S3/GCS/Azure) yourself.
Network Quality API
- Provides per-participant network quality levels (0–5) and stats.
- Use it to:
- Adapt bitrate/resolution
- Trigger UI warnings
- Decide when to switch to audio-only
- Combine with bandwidth profiles for predictable behavior.
Bandwidth Profiles and Track Priority
- Bandwidth profiles define how Twilio allocates bandwidth among tracks.
- Track priority (
low/standard/high) influences which tracks degrade first. - Use for:
- Prioritizing presenter video
- Keeping audio stable under congestion
Webhooks and callbacks
Common callback types:
- Composition status callbacks
- Room status callbacks (where configured)
- Participant events (often handled client-side; server can poll REST API)
Webhooks must be:
- HTTPS
- Verified (Twilio signature validation)
- Idempotent (Twilio retries)
Installation & Setup
Official Python SDK — Video
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
Ubuntu 22.04 LTS (x86_64)
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 twilio-cli@5.5.1
twilio --version
twilio plugins:install @twilio-labs/plugin-video@0.7.0
Fedora 39/40 (x86_64)
sudo dnf install -y nodejs npm jq
node -v
npm -v
sudo npm i -g twilio-cli@5.5.1
twilio plugins:install @twilio-labs/plugin-video@0.7.0
macOS (Intel + Apple Silicon)
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 twilio-cli@5.5.1
twilio plugins:install @twilio-labs/plugin-video@0.7.0
Windows 11 (PowerShell)
Install Node.js 20 LTS from official installer or winget:
winget install OpenJS.NodeJS.LTS
node -v
npm -v
npm i -g twilio-cli@5.5.1
twilio --version
twilio plugins:install @twilio-labs/plugin-video@0.7.0
Server library setup (Node.js)
mkdir -p video-service && cd video-service
npm init -y
npm i twilio@4.23.0 fastify@4.26.2 @fastify/env@4.3.0 pino@8.19.0
npm i -D typescript@5.3.3 tsx@4.7.0 @types/node@20.11.19
Minimal tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Server library setup (Python)
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
Key Capabilities
Room lifecycle management
Production patterns:
- Create rooms on-demand (or pre-provision for scheduled sessions).
- Enforce
uniqueNamenaming conventions: include tenant + resource id. - Set
maxParticipantsto control cost and prevent abuse. - Complete rooms explicitly when sessions end to stop billing.
Example naming scheme:
acme-prod:class:9f3c2b1atenantId:resourceType:resourceId
Node: 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" });
Access token minting (VideoGrant)
Token service requirements:
- Authenticate caller (your auth, not Twilio’s).
- Authorize access to a room (tenant + role checks).
- Mint short-lived token (e.g., 15–60 minutes).
- Rotate API keys without downtime (support multiple signing keys).
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()
Track publication, subscription, and moderation
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:
- “Mute” is typically enforced by client policy (disable local track) + server-side role enforcement.
- For hard enforcement, disconnect participant:
await client.video.v1.rooms("RM...").participants("PA...")
.update({ status: "disconnected" });
Recording and Compositions
Typical flow:
- Enable participant recording (if required) or rely on composition sources.
- Create Composition when room completes (or on-demand).
- Receive status callback (
processing→completed/failed). - Fetch composition media URL and ingest into storage.
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);
Network Quality API usage
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).
Bandwidth and codec tuning
Production defaults:
- Prefer VP8 for broad compatibility; consider H.264 for Safari-heavy audiences.
- Use
maxSubscriptionBitrateto cap downstream usage. - Use track priority for presenter:
const videoTrack = Array.from(room.localParticipant.videoTracks.values())[0]?.track;
videoTrack?.setPriority("high");
Webhook handling (Composition callbacks)
Requirements:
- Validate Twilio signature (
X-Twilio-Signature) - Idempotency: dedupe by
CompositionSid+Status - Retry-safe: Twilio retries on non-2xx
Fastify 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.
Command Reference
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.
Twilio CLI: global flags
Common across commands:
-o, --output [json|tsv|table]output format--properties <props>comma-separated properties to display--no-headeromit 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
Rooms (REST via CLI)
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
Participants
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
Compositions
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
Recordings (if applicable to your account/features)
List recordings for a room:
twilio api:video:v1:recordings:list \
--grouping-sid RM0123456789abcdef0123456789abcdef \
--limit 50 \
-o table \
--properties sid,status,trackName,dateCreated
API Keys (rotation support)
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
Configuration Reference
1) OpenClaw service env file
Path (Linux systemd pattern):
/etc/openclaw/video-service.env
Example:
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
2) systemd unit
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
3) NGINX reverse proxy (TLS + webhook path)
Path:
/etc/nginx/sites-available/video.acme.com- symlink to
/etc/nginx/sites-enabled/video.acme.com
server {
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;
}
}
4) Twilio CLI config
Path:
~/.twilio-cli/config.json
Example 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"
}
}
}
Integration Patterns
Compose with Twilio Verify (step-up auth before joining a room)
Pattern:
- User logs in with your primary auth.
- For high-risk actions (joining a paid session, recording-enabled room), require Verify challenge.
- Only mint Video token after Verify success.
Pseudo-flow:
POST /verify/start→ Twilio Verify V2POST /verify/checkPOST /video/token(requires verified session)
Operational notes:
- Rate limit token minting per user/IP.
- Store Verify decision with TTL (e.g., 10 minutes).
Compose with Programmable Messaging (session notifications)
Use Messaging to send:
- “Room starting” reminders
- “Recording available” link after composition completes
Production requirements:
- Handle STOP/opt-out
- Status callbacks for delivery failures
- 10DLC registration (US A2P) if using long codes
Pipeline example:
- Composition callback
completed - Store media in S3
- Send SMS with link
- Track delivery via status webhook
Compose with SendGrid (recording delivery + audit)
SendGrid transactional email:
- Dynamic template with recording link, session metadata, retention policy
- Handle bounces/spam; suppress invalid recipients
Compose with Studio (orchestration)
Use Studio Flow to orchestrate:
- Pre-session reminders
- Post-session surveys
- Escalation to support if composition fails
Trigger Studio via REST Trigger API from your backend when room completes.
CI/CD pipeline (key rotation + smoke tests)
- Rotate API keys monthly:
- Create new key
- Deploy with dual-key support (active + next)
- Switch signing key
- Revoke old key after TTL window
Smoke test script:
- Create room
- Mint token
- (Optional) headless connect test (Playwright) for JS SDK
- Complete room
- Create composition
- Verify callback received
Error Handling & Troubleshooting
Include these in runbooks; ensure logs capture Twilio request IDs.
1) Auth failure (bad credentials)
Error (REST):
TwilioRestException: Authenticate
HTTP 401
{"code":20003,"message":"Authenticate","more_info":"https://www.twilio.com/docs/errors/20003","status":401}
Root causes:
- Wrong API Key/Secret
- Using API Key SID with Auth Token as password
- Account SID mismatch in client initialization
Fix:
- Verify
TWILIO_ACCOUNT_SID,TWILIO_API_KEY,TWILIO_API_SECRET - Ensure helper library is initialized with
accountSidwhen using API keys - Rotate compromised keys
2) Rate limiting
Error:
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:
- Bursty room/participant polling
- Excessive composition creation retries
Fix:
- Add exponential backoff + jitter
- Cache room state; avoid tight polling loops
- Batch operations; reduce list calls
- Implement circuit breaker for Twilio API
3) Invalid room name / duplicate uniqueName
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:
- Reusing
uniqueNamefor concurrent sessions - Not completing rooms
Fix:
- Use unique per-session names (include timestamp/UUID)
- Complete rooms on session end
- If you need stable names, fetch existing room by uniqueName and reuse only if
completed
4) Token rejected (clock skew / expired)
Client error (JS SDK):
TwilioError: Access Token expired or invalid
Root causes:
- Server clock skew
- TTL too short
- Token minted with wrong signing key secret
Fix:
- Ensure NTP on token service
- Increase TTL to 3600 and refresh proactively
- Validate key rotation logic
5) Webhook signature validation fails
Your service logs:
invalid signature
Root causes:
- Using wrong URL in validation (must match exact public URL)
- Missing/incorrect
TWILIO_AUTH_TOKEN - Proxy modifies URL/host; mismatch between internal and external URL
Fix:
- Use externally visible URL (including scheme/host/path) in validation
- Ensure NGINX sets
HostandX-Forwarded-Proto - Log computed URL and compare to Twilio request
6) Composition stuck in processing / never completes
Composition status remains processing for extended time.
Root causes:
- Very large room duration
- Layout configuration issues
- Source tracks missing or ended unexpectedly
- Twilio-side processing delays
Fix:
- Ensure room completed before composition creation (or accept longer processing)
- Use
audioSources: "*"andvideo_sources: ["*"]for broad capture - Add watchdog: if
processing> threshold (e.g., 2 hours), alert and open Twilio support ticket with Composition SID
7) Participant cannot connect behind corporate firewall
Client logs show ICE failures; typical symptom: “Connecting…” then disconnect.
Root causes:
- UDP blocked; TURN over TCP/TLS required
- Deep packet inspection interfering with WebRTC
Fix:
- Ensure outbound TCP 443 allowed (Twilio TURN over TLS)
- Provide guidance to allowlist Twilio domains
- Consider fallback to audio-only or PSTN (Twilio Voice) for extreme networks
8) Media permissions denied (browser)
Browser error:
NotAllowedError: Permission denied
Root causes:
- User denied mic/camera
- Insecure context (not HTTPS)
- iOS Safari requires user gesture to start capture
Fix:
- Enforce HTTPS
- Prompt with clear UI and retry
- Request permissions after user interaction
9) “Participant identity already exists” (duplicate identity in same room)
Some SDKs surface:
TwilioError: Participant identity is already in use
Root causes:
- Reconnecting with same identity without disconnecting old session
- Multi-tab join without unique identity suffix
Fix:
- Enforce single session per identity (kick old participant)
- Use identity scheme:
userId:deviceIdand map to user in app layer
10) 21211 invalid To (cluster pattern relevance)
If you send SMS notifications and see:
{"code":21211,"message":"The 'To' number +1555... is not a valid phone number.","status":400}
Fix:
- Normalize E.164
- Validate phone numbers before sending
- Store verified numbers only
Security Hardening
Secrets management
- Store
TWILIO_API_SECRETandTWILIO_AUTH_TOKENin a secrets manager:- AWS Secrets Manager / GCP Secret Manager / Vault
- Do not bake secrets into container images.
- Rotate API keys regularly; revoke unused keys.
Webhook verification
- Always validate
X-Twilio-Signature. - Enforce strict path matching; reject unexpected methods.
- Implement idempotency keys:
CompositionSid + Statusunique constraint in DB
Least privilege and isolation
- Separate services:
- Token minting service (API Key Secret)
- Webhook verification service (Auth Token)
- Use distinct Twilio API keys per environment and per service.
TLS and headers
- Enforce TLS 1.2+ (prefer TLS 1.3).
- HSTS for your domain.
- For web apps: CSP, Permissions-Policy for camera/mic.
OS hardening (CIS-aligned)
For Linux hosts (CIS Ubuntu 22.04 LTS guidance):
- Disable password SSH auth; use keys.
- Enable automatic security updates.
- Restrict outbound egress where possible (Twilio endpoints + your storage).
- systemd hardening (see unit file:
NoNewPrivileges,ProtectSystem=strict, etc.).
Abuse prevention
- Rate limit token endpoint by:
- user id
- IP
- room id
- Require authenticated session; never mint tokens anonymously.
- Validate room membership server-side; do not trust client-provided room names.
Performance Tuning
Reduce API calls (measurable)
Before: polling room participants every 2 seconds per active room.
- 500 rooms → 250 req/s → triggers 20429.
After: event-driven + cached state:
- Poll only on state transitions (room created/completed) or on-demand admin actions.
- Expected reduction: >90% API traffic.
Implementation:
- Cache room metadata in Redis with TTL.
- Use client-side events for participant join/leave; send to backend via your own websocket if needed.
Bandwidth profile tuning
Set maxSubscriptionBitrate to cap downstream:
- Default (uncapped): can exceed 4–6 Mbps in grid views.
- Tuned:
2.5 Mbpscap for collaboration. - Expected impact: fewer freezes on mid-tier connections; lower egress cost.
Track priority for presenter
- Set presenter video to
high, othersstandard/low. - Expected impact: presenter remains stable under congestion; non-critical tiles degrade first.
Composition cost control
- Only create compositions when needed:
- user requested recording
- compliance requirement
- Avoid composing every room by default.
Client-side capture constraints
Use 720p for presenter, 360p for others:
const localVideoTrack = await Video.createLocalVideoTrack({
width: 1280,
height: 720,
frameRate: 24
});
Expected impact:
- Lower CPU on clients vs 1080p
- Better stability on integrated GPUs
Advanced Topics
Multi-region considerations
- Twilio Video media region selection may affect latency.
- Keep your token service region-agnostic but consider routing users to nearest region via room configuration (where supported).
- Measure RTT and join time by geography; store metrics per ISP.
Key rotation without downtime
Pattern:
- Maintain
ACTIVE_SIGNING_KEY_SIDandNEXT_SIGNING_KEY_SID. - Mint tokens with active key; accept tokens signed by either during rotation window.
- Rotate:
- Create new key
- Deploy config with both keys
- Switch active
- Wait > max TTL
- Revoke old key
Idempotent composition creation
If your “room completed” handler can run twice:
- Use DB unique constraint on
roomSidfor composition job. - If composition already exists, skip creation.
Handling reconnect storms
When a network blip occurs, many clients reconnect simultaneously.
Mitigations:
- Token endpoint rate limiting with burst allowance
- Client backoff before reconnect
- Avoid creating new rooms on reconnect; reuse same room name
Safari/iOS constraints
- Autoplay restrictions: require user gesture before starting audio.
- Camera switching behavior differs; test with iOS 17+.
- H.264 often more reliable on Safari; validate codec negotiation.
Data tracks for control plane
Use data tracks for:
- “raise hand”
- “mute request”
- “layout hints”
But do not rely on them for authoritative moderation; enforce server-side policy.
Usage Examples
1) Create a scheduled group room, mint tokens, and enforce max participants
Steps:
- Backend creates room at schedule time.
- Users request token; backend checks enrollment.
- Client connects with bandwidth profile.
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
});
2) Moderator disconnects abusive participant
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:
- actor identity
- participant identity
- timestamp
- reason code
3) Post-session composition + SMS notification (Messaging cluster pattern)
Flow:
- Room completed.
- Create composition.
- On callback
completed, store media URL, send SMS.
Composition callback handler:
- Verify signature
- If
Status=completed, enqueue jobsend_recording_sms
SMS send (ensure STOP handling + status callbacks in your messaging service).
4) Network quality-driven UI downgrade to audio-only
Client:
- Subscribe to
networkQualityLevelChanged. - If level <= 1 for > 10 seconds:
- disable local video track
- show banner
- When level >= 3 for > 20 seconds:
- re-enable video
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;
}
});
5) Key rotation drill (monthly)
- Create new API key.
- Deploy token service with both secrets.
- Switch active signing key.
- Validate new tokens connect.
- After 2 hours (if TTL=3600), revoke old key.
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...
6) Incident: 20429 rate limit during peak
Mitigation steps:
- Immediately increase backoff in API callers.
- Disable non-essential polling endpoints.
- Use cached room state for dashboards.
- Postmortem: identify hot loops (participants:list) and replace with event ingestion.
Quick Reference
| 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 |
Graph Relationships
DEPENDS_ON
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, alerting
COMPOSES
twilio-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 support
SIMILAR_TO
zoom-video-sdk(conceptually similar: rooms/sessions, tokens, recording)agora-rtc(tracks, channel join, bandwidth profiles)daily-webrtc(rooms, recording, webhooks)