MCPInstall

twilio-whatsapp

WhatsApp Business: template messages, session messages, media, webhooks, opt-in management

Installs
83
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/alphaonedev/openclaw-graph --skill twilio-whatsapp
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads twilio-whatsapp automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

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:

  1. API Key (recommended):

    • TWILIO_API_KEY_SID (starts with SK...)
    • TWILIO_API_KEY_SECRET
    • TWILIO_ACCOUNT_SID (starts with AC...)
  2. 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)

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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="<Response></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:

  1. Twilio → POST /twilio/inbound
  2. API → publish to Kafka topic twilio.inbound.v1
  3. Worker → parse, apply opt-out, route to conversation service
  4. 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.

2) 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.

3) 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.

4) 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.

5) 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.

6) 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.

7) 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.

8) 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.

9) 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.

10) 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

2) 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.

3) 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.

4) 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.).

5) 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:

  1. User opts in on website checkout.
  2. Send template “shipping_update”.
  3. User replies “Where is my package?”
  4. 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

2) Media: send invoice PDF with presigned URL

Python:

pdf_url = "https://files.example.com/presigned/invoices/18473.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&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)

3) 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."

4) 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

5) Local dev: ngrok + sandbox

  1. Start server on port 3000.
  2. Start ngrok:
ngrok http 3000
  1. Set PUBLIC_BASE_URL to ngrok HTTPS URL.
  2. Configure Twilio sandbox inbound webhook to:
    • https://<id>.ngrok-free.app/twilio/inbound
  3. Send WhatsApp message to sandbox number; verify inbound handler logs.

6) 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

TaskCommand / APIKey flags/fields
Send session texttwilio api:core:messages:create--to, --messaging-service-sid, --body, --status-callback
Send mediamessages.createmedia_url[] / --media-url
Send template (Content API)messages.createcontentSid, contentVariables
List messagestwilio api:core:messages:list--to, --from, --date-sent, --limit
Fetch messagetwilio api:core:messages:fetch--sid
Validate webhookSDK validatorX-Twilio-Signature, exact URL, TWILIO_AUTH_TOKEN
Handle opt-outinbound parsingSTOP/START keywords + suppression list
Diagnose webhook failuresTwilio Console Debuggererror 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)

FAQ

What does the twilio-whatsapp skill do?

WhatsApp Business: template messages, session messages, media, webhooks, opt-in management

How do I install twilio-whatsapp?

Run npx skills add https://github.com/alphaonedev/openclaw-graph --skill twilio-whatsapp in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/twilio-whatsapp/ (personal) or .claude/skills/twilio-whatsapp/ (project).

Which AI agents can use twilio-whatsapp?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is twilio-whatsapp safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains twilio-whatsapp?

It is published by alphaonedev in alphaonedev/openclaw-graph. It has 83 recorded installs.

More from alphaonedev/openclaw-graph

View all →

playwright-scraper

alphaonedev/openclaw-graph

Playwright web scraping: dynamic content, auth flows, pagination, data extraction, screenshots

Testing & QA1.6K installs

humanize-ai-text

alphaonedev/openclaw-graph

Open the skill to read its SKILL.md instructions.

463 installs

gcp-iam

alphaonedev/openclaw-graph

Open the skill to read its SKILL.md instructions.

DevOps & Cloud431 installs

macos-automation

alphaonedev/openclaw-graph

Open the skill to read its SKILL.md instructions.

food-order

alphaonedev/openclaw-graph

Open the skill to read its SKILL.md instructions.

249 installs

tavily-web-search

alphaonedev/openclaw-graph

Open the skill to read its SKILL.md instructions.

Popular business & finance skills

View category →

sales-enablement

coreyhaines31/marketingskills

When the user wants to create sales collateral, pitch decks, one-pagers, objection handling docs, or demo scripts. Also use when the user mentions 'sales deck,' 'pitch deck,' 'one-pager,' 'leave-behind,' 'objection handling,' 'deal-specific ROI analysis,' 'demo script,' 'talk track,' 'sales playbook,' 'proposal template,' 'buyer persona card,' 'battle card,' 'battlecard,' 'competitive one-pager,' 'win-loss analysis,' 'why are we losing deals,' 'loss reasons,' 'how do I respond to this objection,' 'prospect just said,' 'help my sales team,' 'sales materials,' or 'what should I give my sales reps,' 'cold call script,' or 'voicemail script.' Use this for any document or asset that helps a sales team close deals. For public competitor comparison pages, see competitors. For running buyer interviews or churn research, see customer-research. For marketing website copy, see copywriting. For cold outreach emails, see cold-email. For the offer being sold (bonuses, guarantees, pricing structure),

Business & Finance119.5K installs

revops

coreyhaines31/marketingskills

When the user wants help with revenue operations, lead lifecycle management, or marketing-to-sales handoff processes. Also use when the user mentions 'RevOps,' 'revenue operations,' 'lead scoring,' 'outbound pipeline stages,' 'lead routing,' 'MQL,' 'SQL,' 'pipeline stages,' 'deal desk,' 'CRM automation,' 'marketing-to-sales handoff,' 'data hygiene,' 'leads aren't getting to sales,' 'pipeline management,' 'lead qualification,' or 'when should marketing hand off to sales.' Use this for anything involving the systems and processes that connect marketing to revenue. For cold outreach emails, see cold-email. For email drip campaigns, see emails. For pricing decisions, see pricing.

Business & Finance114.1K installs

sales-funnel-blueprint

autonnel/autonnel-skills

Turn an offer into a concrete multi-step sales funnel spec - page-by-page structure, price ladder, copy outline and the metrics each step must hit. Use when asked to build a sales funnel, marketing funnel, landing page flow, lead magnet funnel, webinar funnel, tripwire or VSL funnel, when planning a product launch page flow, or when someone asks "what pages do I need" for selling something online.

Business & Finance104.5K installs

pricing

coreyhaines31/marketingskills

When the user wants help with pricing decisions, packaging, or monetization strategy. Also use when the user mentions 'pricing,' 'pricing tiers,' 'freemium,' 'free trial,' 'packaging,' 'price increase,' 'value metric,' 'Van Westendorp,' 'willingness to pay,' 'monetization,' 'how much should I charge,' 'my pricing is wrong,' 'pricing page,' 'annual vs monthly,' 'per seat pricing,' 'should I offer a free plan,' 'pricing page teardown,' 'pricing page audit,' 'is my pricing page AI-readable,' or 'can AI read my pricing.' Use this whenever someone is figuring out what to charge, how to structure their plans, or wants to audit a pricing page (for humans and for the AI agents that shortlist tools). For in-app upgrade screens, see paywalls. For offer construction (bonuses, guarantees, value framing, naming) on services/courses/coaching/high-ticket B2B, see offers.

Business & Finance80K installs

cross-border-ecommerce

nexscope-ai/ecommerce-skills

Cross-border e-commerce expansion advisor. Scores target markets on 8 weighted dimensions (market size, ecommerce penetration, competition, regulatory complexity, logistics infrastructure, payment ecosystem, cultural distance, IP protection), compares 5 fulfillment models with cost and transit data, provides country-by-country tax/duty compliance guides (EU VAT/IOSS, UK VAT, US sales tax, CA GST, AU GST, JP consumption tax), maps local payment preferences by market, and builds a phased expansion roadmap. No API key required.

Business & Finance62.9K installs

pricing-strategy

coreyhaines31/marketingskills

When the user wants help with pricing decisions, packaging, or monetization strategy. Also use when the user mentions 'pricing,' 'pricing tiers,' 'freemium,' 'free trial,' 'packaging,' 'price increase,' 'value metric,' 'Van Westendorp,' 'willingness to pay,' 'monetization,' 'how much should I charge,' 'my pricing is wrong,' 'pricing page,' 'annual vs monthly,' 'per seat pricing,' or 'should I offer a free plan.' Use this whenever someone is figuring out what to charge or how to structure their plans. For in-app upgrade screens, see paywall-upgrade-cro.

Business & Finance61.2K installs