twilio-whatsapp

Enable OpenClaw to implement and operate Twilio WhatsApp Business messaging in production:

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "twilio-whatsapp" with this command: npx skills add alphaonedev/openclaw-graph/alphaonedev-openclaw-graph-twilio-whatsapp

twilio-whatsapp

Purpose

Enable OpenClaw to implement and operate Twilio WhatsApp Business messaging in production:

  • Send template messages (pre-approved) and session messages (24-hour customer care window).

  • Attach media (images/docs/audio) with correct MIME types and size constraints.

  • Receive and validate webhooks (incoming messages + message status callbacks).

  • Implement opt-in/opt-out and compliance controls (STOP handling, consent logging, regional constraints).

  • Operate reliably under Twilio production constraints: rate limits, retries, idempotency, error codes, and cost controls via Messaging Services.

Concrete value to an engineer: ship a WhatsApp messaging subsystem that is observable, compliant, resilient to webhook retries, and safe to run at scale with predictable failure modes.

Prerequisites

Accounts & Twilio-side setup

  • Twilio account with WhatsApp sender enabled:

  • Either Twilio Sandbox for WhatsApp (dev only) or a WhatsApp Business Profile connected to Twilio (prod).

  • A Twilio Messaging Service (recommended for production) with:

  • WhatsApp sender(s) attached (e.g., whatsapp:+14155238886 or your approved WA number).

  • Status callback URL configured (optional but recommended).

  • WhatsApp templates approved in Meta Business Manager (via Twilio Console template manager).

Local tooling versions (pinned)

  • Node.js 20.11.1 (LTS) or 18.19.1 (LTS)

  • Python 3.11.8 or 3.12.2

  • Twilio helper libraries:

  • twilio (Node) 4.23.0

  • twilio (Python) 9.0.5

  • Twilio CLI 5.16.0 (for diagnostics; not required at runtime)

  • ngrok 3.13.1 (local webhook testing)

Auth & secrets

Use one of:

API Key (recommended):

  • TWILIO_API_KEY_SID (starts with SK... )

  • TWILIO_API_KEY_SECRET

  • TWILIO_ACCOUNT_SID (starts with AC... )

Account SID + Auth Token:

  • TWILIO_ACCOUNT_SID

  • TWILIO_AUTH_TOKEN

Store secrets in:

  • Kubernetes: Secret
  • mounted env vars
  • AWS: Secrets Manager + IRSA

  • GCP: Secret Manager + Workload Identity

  • Local dev: .env (never commit)

Network & webhook requirements

  • Public HTTPS endpoint for webhooks (Twilio requires HTTPS in most production contexts).

  • Allow inbound from Twilio webhook IPs is not stable; validate using X-Twilio-Signature instead of IP allowlists.

  • Ensure your endpoint can handle retries and out-of-order delivery.

Core Concepts

WhatsApp message types (Twilio perspective)

Template message (outside 24-hour window):

  • Must use a pre-approved template.

  • Used for notifications, OTP, shipping updates, etc.

  • In Twilio, templates are typically sent via the Content API (preferred) or via template integration depending on account configuration.

Session message (inside 24-hour window):

  • Free-form text/media allowed (subject to WhatsApp policies).

  • The 24-hour window starts when the user messages you.

Media message:

  • WhatsApp supports images, documents, audio, video with constraints.

  • Twilio sends media via MediaUrl (publicly accessible URL) or via Twilio-hosted media in some flows.

Identifiers and addressing

  • WhatsApp addresses in Twilio use whatsapp: prefix:

  • From : whatsapp:+14155238886

  • To : whatsapp:+14155550123

  • Messaging Service SID: MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

  • Message SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Webhooks

Two primary webhook categories:

Incoming message webhook (when user sends you a message):

  • Twilio sends an HTTP request with form-encoded parameters like From , To , Body , NumMedia , MediaUrl0 , etc.

Message status callback (delivery lifecycle):

  • Status values: queued , sent , delivered , read , failed , undelivered

  • Twilio retries on non-2xx responses with backoff.

Idempotency and retries

  • Twilio may retry webhooks; your handler must be idempotent.

  • Status callbacks can arrive out of order (e.g., delivered then read , or failed after transient states).

  • Use MessageSid

  • MessageStatus
  • timestamp to dedupe.

Compliance: opt-in/opt-out

  • WhatsApp requires user opt-in; you must store consent evidence.

  • STOP handling:

  • For SMS, Twilio has built-in STOP.

  • For WhatsApp, you must implement opt-out keywords and respect them (e.g., “STOP”, “UNSUBSCRIBE”).

  • Maintain a suppression list keyed by E.164 phone number.

Installation & Setup

Official Python SDK — WhatsApp

Repository: https://github.com/twilio/twilio-python

PyPI: pip install twilio · Supported: Python 3.7–3.13

from twilio.rest import Client client = Client()

Send WhatsApp message (Sandbox: from_ = 'whatsapp:+14155238886')

msg = client.messages.create( body="Your order is confirmed!", from_="whatsapp:+14155238886", to="whatsapp:+15558675309" )

Send template message (approved HSM)

msg = client.messages.create( from_="whatsapp:+14155238886", to="whatsapp:+15558675309", content_sid="HX...", # pre-approved template SID content_variables='{"1":"Alice","2":"12345"}' )

Source: twilio/twilio-python — messages

Ubuntu 22.04 LTS (x86_64)

sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg jq

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 npm -v

Python 3.11:

sudo apt-get install -y python3.11 python3.11-venv python3-pip python3.11 --version

Twilio CLI 5.16.0:

npm install -g twilio-cli@5.16.0 twilio --version

ngrok 3.13.1:

curl -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

Fedora 39 (x86_64)

sudo dnf install -y curl jq nodejs python3 python3-virtualenv node -v python3 --version

Twilio CLI:

sudo npm install -g twilio-cli@5.16.0 twilio --version

ngrok:

sudo dnf install -y ngrok ngrok version

macOS 14 (Sonoma) — Intel + Apple Silicon

Homebrew:

brew update brew install node@20 python@3.12 jq ngrok/ngrok/ngrok node -v python3 --version ngrok version

Twilio CLI:

npm install -g twilio-cli@5.16.0 twilio --version

Auth setup (CLI + env)

Twilio CLI login (writes to ~/.twilio-cli/config.json ):

twilio login

Runtime env vars (recommended: API Key):

export TWILIO_ACCOUNT_SID="AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1" export TWILIO_API_KEY_SID="SK3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8" export TWILIO_API_KEY_SECRET="a_very_long_secret_value" export TWILIO_MESSAGING_SERVICE_SID="MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5"

Local .env (example path: /srv/whatsapp/.env ):

TWILIO_ACCOUNT_SID=AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1 TWILIO_API_KEY_SID=SK3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8 TWILIO_API_KEY_SECRET=a_very_long_secret_value TWILIO_MESSAGING_SERVICE_SID=MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 PUBLIC_BASE_URL=https://wa.example.com

Key Capabilities

Send session messages (text + media)

  • Use Twilio Programmable Messaging Messages API.

  • Ensure To and From include whatsapp: prefix.

  • Prefer MessagingServiceSid over hardcoding From for routing and future sender expansion.

Node (twilio 4.23.0):

import twilio from "twilio";

const client = twilio( process.env.TWILIO_API_KEY_SID, process.env.TWILIO_API_KEY_SECRET, { accountSid: process.env.TWILIO_ACCOUNT_SID } );

export async function sendSessionText(toE164, body) { const msg = await client.messages.create({ to: whatsapp:${toE164}, messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID, body, statusCallback: ${process.env.PUBLIC_BASE_URL}/twilio/status }); return msg.sid; }

Python (twilio 9.0.5):

import os from twilio.rest import Client

client = Client( os.environ["TWILIO_API_KEY_SID"], os.environ["TWILIO_API_KEY_SECRET"], os.environ["TWILIO_ACCOUNT_SID"], )

def send_session_media(to_e164: str, body: str, media_url: str) -> str: msg = client.messages.create( to=f"whatsapp:{to_e164}", messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"], body=body, media_url=[media_url], status_callback=f"{os.environ['PUBLIC_BASE_URL']}/twilio/status", ) return msg.sid

Operational constraints:

  • Media URLs must be publicly reachable by Twilio (no private S3 URL unless presigned).

  • If you send media, validate content-type and size before sending to reduce failures.

Send template messages (outside session window)

Production recommendation: use Twilio Content API (aka “Content Templates”) when available in your account. This decouples template definition from code and supports localization/variables.

Content API send (Messages API with contentSid )

Node:

export async function sendTemplate(toE164) { const msg = await client.messages.create({ to: whatsapp:${toE164}, messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID, contentSid: "HXb5b62575e6e4ff6129ad7c8efe1f983e", contentVariables: JSON.stringify({ "1": "Ava", "2": "Order #18473", "3": "2026-02-21" }), statusCallback: ${process.env.PUBLIC_BASE_URL}/twilio/status }); return msg.sid; }

Python:

import json

def send_template(to_e164: str) -> str: msg = client.messages.create( to=f"whatsapp:{to_e164}", messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"], content_sid="HXb5b62575e6e4ff6129ad7c8efe1f983e", content_variables=json.dumps({"1": "Ava", "2": "Order #18473", "3": "2026-02-21"}), status_callback=f"{os.environ['PUBLIC_BASE_URL']}/twilio/status", ) return msg.sid

Notes:

  • contentVariables keys are strings "1" , "2" , etc. per Twilio Content variable indexing.

  • Template approval and category (utility/marketing/authentication) affects deliverability and policy compliance.

Receive inbound WhatsApp messages (webhook handler)

Twilio sends application/x-www-form-urlencoded by default.

Express (Node):

import express from "express"; import twilio from "twilio";

const app = express(); app.use(express.urlencoded({ extended: false }));

app.post("/twilio/inbound", (req, res) => { const signature = req.header("X-Twilio-Signature") || ""; const url = ${process.env.PUBLIC_BASE_URL}/twilio/inbound;

const isValid = twilio.validateRequest( process.env.TWILIO_AUTH_TOKEN, // validateRequest requires Auth Token, not API key secret signature, url, req.body );

if (!isValid) return res.status(403).send("invalid signature");

const from = req.body.From; // e.g. "whatsapp:+14155550123" const body = req.body.Body || ""; const numMedia = parseInt(req.body.NumMedia || "0", 10);

// Idempotency: inbound messages have MessageSid const messageSid = req.body.MessageSid;

// TODO: persist inbound event, dedupe by MessageSid // TODO: implement opt-out keywords

res.type("text/xml").send("<Response></Response>"); });

app.listen(3000);

Important: validateRequest requires TWILIO_AUTH_TOKEN . If you use API Keys for REST calls, you still need Auth Token for webhook signature validation. Store it separately and restrict access.

FastAPI (Python):

import os from fastapi import FastAPI, Request, Response from twilio.request_validator import RequestValidator

app = FastAPI() validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])

@app.post("/twilio/inbound") async def inbound(request: Request): form = await request.form() signature = request.headers.get("X-Twilio-Signature", "") url = f"{os.environ['PUBLIC_BASE_URL']}/twilio/inbound"

if not validator.validate(url, dict(form), signature):
    return Response(content="invalid signature", status_code=403)

message_sid = form.get("MessageSid")
from_ = form.get("From")
body = form.get("Body", "")

return Response(content="&#x3C;Response>&#x3C;/Response>", media_type="text/xml")

Message status callbacks (delivered/read/failed)

Configure statusCallback per message or at Messaging Service level.

Twilio will POST fields including:

  • MessageSid , MessageStatus , To , From , ErrorCode , ErrorMessage

Handler requirements:

  • Always return 2xx quickly (under ~2 seconds).

  • Enqueue processing to a job queue (SQS, Pub/Sub, Kafka).

  • Dedupe by (MessageSid, MessageStatus) .

Example (Express):

app.post("/twilio/status", (req, res) => { // Validate signature same as inbound const { MessageSid, MessageStatus, ErrorCode, ErrorMessage } = req.body;

// Persist status transition; do not assume ordering // If failed/undelivered, capture ErrorCode + ErrorMessage for triage

res.sendStatus(204); });

Opt-in management and suppression

Implement:

  • Consent capture (timestamp, source, IP/user agent if applicable, proof text).

  • Suppression list:

  • If user sends “STOP”, “UNSUBSCRIBE”, “CANCEL”, “END”, “QUIT” → mark suppressed.

  • If user sends “START”, “UNSTOP”, “SUBSCRIBE” → unsuppress (only if policy allows).

Example keyword parsing:

STOP_WORDS = {"stop", "unsubscribe", "cancel", "end", "quit"} START_WORDS = {"start", "unstop", "subscribe"}

def classify_opt(body: str) -> str | None: t = body.strip().lower() if t in STOP_WORDS: return "STOP" if t in START_WORDS: return "START" return None

Enforcement:

  • Before sending any outbound message, check suppression list.

  • For template messages, also check consent freshness and region-specific rules.

Media handling (upload, validation, and delivery)

Twilio requires MediaUrl accessible by Twilio. Common pattern:

  • Store media in S3 with short-lived presigned URL (e.g., 15 minutes).

  • Validate MIME type and size before generating URL.

Constraints vary; enforce conservative limits:

  • Images: <= 5 MB

  • Documents: <= 100 MB (PDF), but enforce smaller for reliability

  • Audio/video: enforce <= 16 MB unless you have confirmed limits for your account/region

Example: generate presigned URL (AWS SDK v3, Node):

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "us-east-1" });

export async function presign(bucket, key) { const cmd = new GetObjectCommand({ Bucket: bucket, Key: key }); return await getSignedUrl(s3, cmd, { expiresIn: 900 }); }

Webhook replay protection and idempotency

Store processed webhook IDs:

  • Inbound: MessageSid

  • Status: MessageSid + ":" + MessageStatus

Use a fast store (Redis) with TTL (e.g., 7 days) to prevent duplicate processing.

Redis example (pseudo):

SETNX twilio:inbound:SM... 1 EX 604800 SETNX twilio:status:SM...:delivered 1 EX 604800

Command Reference

Twilio CLI (5.16.0)

Authenticate

twilio login

Flags:

  • --profile <name> : store credentials under a named profile

  • --username <AC...> : account SID

  • --password <auth_token> : auth token (interactive if omitted)

List messages

twilio api:core:messages:list

Relevant flags:

  • --to <string> : filter by To (e.g., whatsapp:+14155550123 )

  • --from <string> : filter by From

  • --date-sent <YYYY-MM-DD> : filter by date

  • --page-size <int> : default 50

  • --limit <int> : max records to return

  • --properties <csv> : select fields (CLI dependent)

  • --output json|tsv|csv : output format (CLI dependent)

Example:

twilio api:core:messages:list --to "whatsapp:+14155550123" --limit 20 --output json | jq .

Fetch a message

twilio api:core:messages:fetch --sid SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Flags:

  • --sid <SM...> : required

Create a message (session or template via Content API)

twilio api:core:messages:create
--to "whatsapp:+14155550123"
--messaging-service-sid MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
--body "Hello from production"

All relevant flags (commonly supported by API/CLI; availability may vary by CLI version):

  • --to <string> : required

  • --from <string> : optional if using Messaging Service

  • --messaging-service-sid <MG...> : recommended

  • --body <string> : message text

  • --media-url <url> : repeatable for multiple media

  • --status-callback <url> : status webhook

  • --max-price <decimal> : price cap (channel-dependent)

  • --provide-feedback <boolean> : request delivery feedback (carrier dependent)

  • --attempt <int> / --validity-period <int> : channel dependent; may not apply to WhatsApp

  • --content-sid <HX...> : Content API template identifier

  • --content-variables <json> : JSON string of variables

Example template send:

twilio api:core:messages:create
--to "whatsapp:+14155550123"
--messaging-service-sid MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
--content-sid HXb5b62575e6e4ff6129ad7c8efe1f983e
--content-variables '{"1":"Ava","2":"Order #18473","3":"2026-02-21"}'

Debug webhooks locally with ngrok

ngrok http 3000

Copy the HTTPS forwarding URL into:

  • Twilio Console → Messaging → WhatsApp Sender / Messaging Service → Inbound webhook

  • Status callback URL

Configuration Reference

Node service config

Path: /srv/whatsapp/config/whatsapp.production.toml

[twilio] account_sid = "AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1" messaging_service_sid = "MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5" public_base_url = "https://wa.example.com"

[webhooks] inbound_path = "/twilio/inbound" status_path = "/twilio/status" validate_signatures = true signature_auth_token_env = "TWILIO_AUTH_TOKEN"

[opt] stop_keywords = ["stop","unsubscribe","cancel","end","quit"] start_keywords = ["start","unstop","subscribe"] suppression_ttl_days = 3650

[media] max_image_bytes = 5242880 max_doc_bytes = 26214400 presign_ttl_seconds = 900 allowed_mime_prefixes = ["image/","application/pdf"]

Path: /srv/whatsapp/.env (permissions 0600 )

TWILIO_ACCOUNT_SID=AC2f7b9c2b0f1d2e3a4b5c6d7e8f9a0b1 TWILIO_API_KEY_SID=SK3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8 TWILIO_API_KEY_SECRET=a_very_long_secret_value TWILIO_AUTH_TOKEN=your_auth_token_for_signature_validation TWILIO_MESSAGING_SERVICE_SID=MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 PUBLIC_BASE_URL=https://wa.example.com

systemd unit (Linux)

Path: /etc/systemd/system/whatsapp.service

[Unit] Description=WhatsApp Messaging Service After=network-online.target Wants=network-online.target

[Service] Type=simple User=whatsapp Group=whatsapp WorkingDirectory=/srv/whatsapp EnvironmentFile=/srv/whatsapp/.env ExecStart=/usr/bin/node /srv/whatsapp/dist/server.js Restart=on-failure RestartSec=2 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/srv/whatsapp /var/log/whatsapp LimitNOFILE=65535

[Install] WantedBy=multi-user.target

Kubernetes deployment snippet

Path: /srv/whatsapp/deploy/k8s/deployment.yaml

apiVersion: apps/v1 kind: Deployment metadata: name: whatsapp namespace: messaging spec: replicas: 6 selector: matchLabels: app: whatsapp template: metadata: labels: app: whatsapp spec: containers: - name: whatsapp image: ghcr.io/acme/whatsapp:2026.02.21 ports: - containerPort: 3000 env: - name: TWILIO_ACCOUNT_SID valueFrom: secretKeyRef: name: twilio key: account_sid - name: TWILIO_API_KEY_SID valueFrom: secretKeyRef: name: twilio key: api_key_sid - name: TWILIO_API_KEY_SECRET valueFrom: secretKeyRef: name: twilio key: api_key_secret - name: TWILIO_AUTH_TOKEN valueFrom: secretKeyRef: name: twilio key: auth_token - name: TWILIO_MESSAGING_SERVICE_SID valueFrom: secretKeyRef: name: twilio key: messaging_service_sid - name: PUBLIC_BASE_URL value: "https://wa.example.com" readinessProbe: httpGet: path: /healthz port: 3000 initialDelaySeconds: 3 periodSeconds: 5 resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "1" memory: "1Gi"

Integration Patterns

Pattern: API service + queue for webhook processing

  • Webhook handler validates signature and enqueues event.

  • Worker consumes events and updates DB, triggers downstream actions.

Example pipeline:

  • Twilio → POST /twilio/inbound

  • API → publish to Kafka topic twilio.inbound.v1

  • Worker → parse, apply opt-out, route to conversation service

  • Conversation service → decides response → sends via Twilio Messages API

Kafka message schema (JSON):

{ "event_type": "twilio_inbound", "received_at": "2026-02-21T18:22:11.123Z", "message_sid": "SMd2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7", "from": "whatsapp:+14155550123", "to": "whatsapp:+14155238886", "body": "Where is my order?", "num_media": 0, "raw": { "Body": "Where is my order?", "ProfileName": "Ava" } }

Pattern: Compose with Twilio Verify (OTP over WhatsApp)

  • Use Verify V2 with WhatsApp channel where supported.

  • Fallback to SMS if WhatsApp fails.

Flow:

  • Attempt Verify via WhatsApp

  • If error indicates channel unavailable, fallback to SMS

Key operational note: Verify has its own rate limiting and fraud controls; do not DIY OTP over session messages unless you accept the compliance and abuse risk.

Pattern: Compose with SendGrid for email fallback

  • If WhatsApp template fails with 63016 (outside window / template required) or user opted out:

  • Send transactional email via SendGrid dynamic template.

  • Keep a unified notification log with channel attempts and outcomes.

Pattern: Studio for rapid iteration, API for core flows

  • Use Twilio Studio for low-risk flows (FAQ, routing).

  • Use REST Trigger API to start Studio flows from your backend.

  • Keep WhatsApp sending in code for high-volume, audited flows.

Error Handling & Troubleshooting

Handle Twilio errors at two layers:

  • REST API errors when sending

  • Webhook status callbacks with ErrorCode /ErrorMessage

At minimum, log:

  • MessageSid , To , From , MessagingServiceSid , ErrorCode , ErrorMessage , HTTP status, Twilio request ID (X-Twilio-Request-Id if present)
  1. 21211 — invalid To number

Error text (common):

TwilioRestException: The 'To' number +1415555012 is not a valid phone number.

Root cause:

  • Not E.164, missing digits, or missing whatsapp: prefix.

Fix:

  • Normalize to E.164 and prefix: To=whatsapp:+14155550123 .

  • Validate with libphonenumber before calling Twilio.

  1. 20003 — authentication failure

Error text:

Authenticate The AccountSid and AuthToken combination you have provided is invalid.

Root cause:

  • Wrong credentials, mixing API key secret with auth token, wrong account SID.

Fix:

  • For REST calls with API keys: use (apiKeySid, apiKeySecret, accountSid) .

  • For webhook validation: use TWILIO_AUTH_TOKEN .

  • Rotate compromised credentials; verify environment injection.

  1. 20429 — rate limit exceeded

Error text:

Too Many Requests Rate limit exceeded

Root cause:

  • Bursting sends, too many concurrent API calls, or account-level limits.

Fix:

  • Implement client-side rate limiting (token bucket).

  • Batch sends and use a queue with concurrency control.

  • Prefer Messaging Service and distribute across senders if applicable.

  1. 30003 — Unreachable destination / carrier violation (often SMS, can surface in mixed services)

Error text:

Message Delivery - Carrier violation

Root cause:

  • Carrier filtering, invalid destination, or blocked route.

Fix:

  • For WhatsApp, ensure destination is WhatsApp-capable and opted in.

  • Verify sender is approved for the destination region.

  • Check status callback ErrorCode and Twilio Console logs.

  1. 63016 — WhatsApp template required / outside session window

Common status callback error message:

63016: Failed to send message because you are outside the allowed window.

Root cause:

  • Attempted free-form session message outside 24-hour window.

Fix:

  • Use an approved template (Content API) to re-open conversation.

  • Track last inbound user message timestamp per user.

  1. 63018 — Template not found / not approved / mismatch

Common error:

63018: Content template not found or not approved for use.

Root cause:

  • Wrong contentSid , template not approved, or not enabled for WhatsApp sender.

Fix:

  • Verify template approval status in Twilio Console.

  • Ensure template is associated with the correct WhatsApp sender / business.

  • Deploy template changes before code rollout.

  1. 21610 — user opted out (more typical for SMS; still handle suppression uniformly)

Error text:

Attempt to send to unsubscribed recipient

Root cause:

  • Recipient opted out (Twilio-managed for SMS) or your own suppression list for WhatsApp.

Fix:

  • For WhatsApp: enforce your suppression list before sending.

  • Provide a re-subscribe path and record consent.

  1. Webhook signature validation failures

Your service logs:

invalid signature

Root cause:

  • PUBLIC_BASE_URL mismatch (ngrok URL changed), wrong auth token, proxy rewriting URL, missing form parsing.

Fix:

  • Ensure the exact URL used in validation matches Twilio’s requested URL (scheme/host/path).

  • In Express, use express.urlencoded({ extended: false }) before handler.

  • If behind a reverse proxy, ensure PUBLIC_BASE_URL matches external URL, not internal.

  1. Media fetch failures

Status callback may show:

30007: Carrier violation

or Twilio console indicates media fetch error.

Root cause:

  • Media URL not publicly accessible, expired presigned URL, blocked by WAF, wrong TLS config.

Fix:

  • Presign with sufficient TTL (>= 10 minutes).

  • Allow Twilio user agent through WAF or bypass for media bucket.

  • Ensure correct Content-Type and Content-Length .

  1. 11200 — HTTP retrieval failure (webhook endpoint)

Twilio debugger shows:

11200 - HTTP retrieval failure

Root cause:

  • Your webhook endpoint timed out, returned 5xx, DNS/TLS issues.

Fix:

  • Return 2xx quickly; enqueue work.

  • Increase server timeouts; ensure TLS chain is correct.

  • Add health checks and autoscaling.

Security Hardening

Webhook validation (mandatory)

  • Validate X-Twilio-Signature on every inbound and status webhook.

  • Keep TWILIO_AUTH_TOKEN in a restricted secret store; do not expose to app logs.

  • If using API keys for REST, still store Auth Token for validation.

Least privilege credentials

  • Prefer API Keys over Auth Token for REST calls.

  • Rotate API keys quarterly; rotate immediately on suspected compromise.

  • Separate keys per environment (dev/stage/prod) and per service.

Transport security

  • Enforce TLS 1.2+ on public endpoints.

  • Use HSTS on your domain.

  • Do not accept plaintext HTTP for webhooks.

Data minimization

  • Store only required message content; consider hashing or redacting:

  • OTP codes

  • Payment details (should never be sent)

  • Sensitive PII

  • Apply retention policies (e.g., 30–90 days for message bodies, longer for metadata).

Access controls and audit

  • Restrict Twilio Console access via SSO and MFA.

  • Log all template changes and sender changes.

  • Use separate subaccounts for isolation if your org structure supports it.

CIS-aligned host hardening (Linux)

Reference: CIS Ubuntu Linux 22.04 LTS Benchmark (where applicable).

  • Run service as non-root user (whatsapp ).

  • systemd hardening:

  • NoNewPrivileges=true

  • ProtectSystem=strict

  • ProtectHome=true

  • PrivateTmp=true

  • File permissions:

  • /srv/whatsapp/.env mode 0600 , owned by service user.

  • Disable shell access for service user:

  • /usr/sbin/nologin

WAF / reverse proxy considerations

  • Do not IP-allowlist Twilio; validate signatures instead.

  • Ensure proxy preserves request body exactly; signature validation is sensitive to parameter changes.

  • If you must transform requests, validate at the edge before transformation.

Performance Tuning

  1. Webhook latency: enqueue + 204

Target:

  • p95 webhook handler latency < 50ms (excluding network)

  • Always respond within 1s

Expected impact:

  • Reduces Twilio retries and duplicate deliveries.

  • Stabilizes under burst traffic.

Implementation:

  • Parse + validate signature

  • Write minimal event record

  • Enqueue job

  • Return 204 No Content

  1. Outbound throughput: concurrency control

Problem:

  • Unbounded concurrency triggers 20429 and increases tail latency.

Solution:

  • Token bucket per sender or per Messaging Service.

  • Start with concurrency 20–50 per pod; tune based on observed 20429 rate.

Expected impact:

  • Fewer rate-limit errors; higher sustained throughput.
  1. Connection reuse
  • Use HTTP keep-alive agent (Node) for Twilio REST calls.

  • In Python, reuse client and avoid creating per-request.

Expected impact:

  • Lower CPU and latency under high send volume.
  1. Dedupe storage
  • Use Redis with SETNX and TTL for webhook dedupe.

  • Keep TTL aligned with your maximum replay window (7–14 days).

Expected impact:

  • Prevents duplicate downstream actions (double replies, double refunds, etc.).
  1. Cost optimization via Messaging Service
  • Use Messaging Service for sender pooling and routing.

  • For mixed channels (SMS/WhatsApp), configure geo-matching and fallback rules carefully.

Expected impact:

  • Lower operational overhead; fewer misroutes; potential cost savings depending on routing.

Advanced Topics

Handling out-of-order status transitions

Do not model status as a simple state machine with strict ordering. Instead:

  • Store all status events with timestamps.

  • Derive “current status” as the max-precedence terminal state:

  • failed /undelivered terminal negative

  • read terminal positive

  • delivered positive

  • sent /queued transient

Multi-tenant / subaccount architecture

If you serve multiple customers:

  • Use Twilio subaccounts per tenant for isolation.

  • Store per-tenant AccountSid and API key.

  • Ensure webhook validation uses the correct Auth Token per tenant (map by To number or AccountSid if provided).

Template localization

  • Use Content API with localized variants.

  • Choose locale based on user profile; fallback to en_US .

  • Keep template variables stable across locales.

Media privacy and compliance

  • Presigned URLs leak access if forwarded; keep TTL short.

  • Consider proxying media through your domain with auth if policy requires, but ensure Twilio can fetch it.

Disaster recovery

  • If webhook processing is down, Twilio will retry for a limited period.

  • Persist raw webhook payloads to durable storage (S3/GCS) for replay.

  • Provide a replay tool that re-enqueues events by MessageSid .

Testing strategy

  • Unit test:

  • Signature validation (known-good fixtures)

  • Opt-out keyword parsing

  • E.164 normalization

  • Integration test:

  • Send message to sandbox number

  • Verify status callback receipt

  • Load test:

  • Simulate webhook bursts (e.g., 500 RPS) and ensure 2xx responses

Usage Examples

  1. Production: send a template for shipping update, then handle replies

Steps:

  • User opts in on website checkout.

  • Send template “shipping_update”.

  • User replies “Where is my package?”

  • Respond with session message.

Node (end-to-end sketch):

// 1) consent stored elsewhere const to = "+14155550123";

// 2) template send const templateSid = "HXb5b62575e6e4ff6129ad7c8efe1f983e"; await client.messages.create({ to: whatsapp:${to}, messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID, contentSid: templateSid, contentVariables: JSON.stringify({ "1": "Ava", "2": "18473", "3": "UPS" }), statusCallback: ${process.env.PUBLIC_BASE_URL}/twilio/status });

// 3/4) inbound webhook routes to agent/bot and responds within 24h window

  1. Media: send invoice PDF with presigned URL

Python:

pdf_url = "https://files.example.com/presigned/invoices/18473.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&#x26;X-Amz-Credential=..." sid = client.messages.create( to="whatsapp:+14155550123", messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"], body="Invoice for Order #18473", media_url=[pdf_url], status_callback=f"{os.environ['PUBLIC_BASE_URL']}/twilio/status", ).sid print(sid)

  1. Opt-out: user sends STOP, enforce suppression

Inbound handler logic:

  • If body is STOP keyword:

  • Mark suppressed

  • Reply confirmation (session message allowed because user initiated)

Example response (TwiML empty is fine; you can also send outbound message via REST):

<Response></Response>

Then send confirmation via REST:

twilio api:core:messages:create
--to "whatsapp:+14155550123"
--messaging-service-sid MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5
--body "You are opted out. Reply START to re-subscribe."

  1. Status-driven retry: transient failure handling

Policy:

  • Do not blindly retry WhatsApp sends on failed without inspecting error code.

  • Retry only on known transient conditions (e.g., 20429 rate limit) with backoff.

Pseudo:

  • If REST call returns 429 or error 20429:

  • retry with exponential backoff + jitter

  • If status callback returns 63016:

  • switch to template message or alternate channel

  1. Local dev: ngrok + sandbox
  • Start server on port 3000.

  • Start ngrok:

ngrok http 3000

  • Set PUBLIC_BASE_URL to ngrok HTTPS URL.

  • Configure Twilio sandbox inbound webhook to:

  • https://<id>.ngrok-free.app/twilio/inbound

  • Send WhatsApp message to sandbox number; verify inbound handler logs.

  1. Multi-region: route by user locale and sender
  • Maintain mapping:

  • country_code -> messaging_service_sid

  • Choose Messaging Service based on To country.

Example mapping file:

Path: /srv/whatsapp/config/routing.yaml

default_messaging_service_sid: MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 by_country: US: MG0c3d2e1f4a5b6c7d8e9f0a1b2c3d4e5 GB: YOUR_MG_SID DE: YOUR_MG_SID

Quick Reference

Task Command / API Key flags/fields

Send session text twilio api:core:messages:create

--to , --messaging-service-sid , --body , --status-callback

Send media messages.create

media_url[] / --media-url

Send template (Content API) messages.create

contentSid , contentVariables

List messages twilio api:core:messages:list

--to , --from , --date-sent , --limit

Fetch message twilio api:core:messages:fetch

--sid

Validate webhook SDK validator X-Twilio-Signature , exact URL, TWILIO_AUTH_TOKEN

Handle opt-out inbound parsing STOP/START keywords + suppression list

Diagnose webhook failures Twilio Console Debugger error 11200 , request/response details

Graph Relationships

DEPENDS_ON

  • twilio-core (Twilio REST API fundamentals: auth, subaccounts, API keys)

  • twilio-messaging (Programmable Messaging patterns: Messaging Services, status callbacks)

  • webhook-security (signature validation, replay protection)

  • queueing (Kafka/SQS/PubSub patterns for async processing)

  • secrets-management (KMS, Vault, cloud secret managers)

COMPOSES

  • twilio-verify (OTP via WhatsApp where supported; fallback strategies)

  • sendgrid-transactional (email fallback when WhatsApp fails or user opted out)

  • twilio-studio (rapid flow prototyping; REST trigger integration)

  • observability (structured logs, tracing, metrics, alerting on failure rates)

SIMILAR_TO

  • twilio-sms (similar send/status patterns; different compliance and STOP semantics)

  • meta-whatsapp-cloud-api (direct Meta API; Twilio abstracts some concerns but adds its own constraints)

  • twilio-conversations (higher-level conversation orchestration; different primitives than raw Messages API)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

playwright-scraper

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clawflows

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tavily-web-search

No summary provided by upstream source.

Repository SourceNeeds Review