Twilio Messaging skill for Claude Code
Texts that get delivered and stay compliant. Steers Claude toward Twilio setups that survive carrier scrutiny: sends routed through Messaging Services with status callbacks, every inbound and status webhook validated against X-Twilio-Signature, per-message delivery state stored and only moved forward, and opt-outs mirrored in your own database. It also covers 10DLC and toll-free registration, WhatsApp template rules and Verify for login codes.
- name
twilio-messaging - size222 lines · ≈2,350 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds SMS, WhatsApp, and voice features on Twilio - sending through Messaging Services, signature-validated inbound and status webhooks, TwiML replies, delivery status tracking and error-code handling, STOP/HELP opt-out compliance, A2P 10DLC and toll-free registration, WhatsApp templates and the 24-hour window, and Verify for OTP. Use when sending texts or WhatsApp messages, receiving inbound SMS or calls, messages show undelivered or 30007/30034 errors, or when adding phone verification codes. Try asking
- Send appointment reminders by SMS with Twilio and track whether they were delivered
- Our Twilio messages fail with error 30034 - what do we need to fix?
- Add phone number verification with one-time codes to signup
Use it when
- Sending SMS or WhatsApp notifications from an app
- Receiving inbound texts or calls through Twilio webhooks
- Messages show undelivered, carrier-filtered, or unregistered errors
- Adding OTP phone verification or STOP/HELP handling
Skip it when
- The channel is email rather than phone - use transactional-email instead
- You are building generic multi-provider webhook infrastructure - use webhook-handling instead
How to check the result
Send to a real handset and watch the status reach delivered through the callback, then post a forged webhook without a valid signature and confirm it is rejected with 403.
The common mistake it prevents
Behind a proxy the app rebuilds the webhook URL as http or an internal host, so the signature never matches and developers disable validation instead of fixing the URL.
Messaging is done when every outbound message goes through a Messaging Service with a status callback, every inbound or status webhook is signature-validated, delivery state is persisted per message, opt-outs are honored before sending, and US traffic runs on a registered sender (10DLC campaign, verified toll-free, or short code).
Ground rules
- Credentials from env only. Prefer an API Key SID + secret over the Account Auth Token in application code; the Auth Token is still needed to validate webhook signatures.
- Validate
X-Twilio-Signatureon every webhook. An unvalidated SMS webhook lets anyone forge inbound messages or delivery receipts. - Send via a Messaging Service SID, not a raw
fromnumber: it handles sender pools, opt-out management, queueing, and sticky sender. - Consent first. Only message numbers that opted in for that purpose, and keep proof (timestamp, source, wording). Read references/compliance.md before launching any US/Canada campaign or marketing messages.
- Never use SMS for secrets beyond short-lived OTPs, and never build your own OTP storage when Verify fits.
Workflow
- [ ] 1. Confirm sender type and registration status (10DLC/toll-free/short code/WhatsApp)
- [ ] 2. Create or reuse a Messaging Service; attach senders; set status callback
- [ ] 3. Implement send with persistence and idempotency
- [ ] 4. Implement inbound + status webhooks with signature validation
- [ ] 5. Handle opt-out/help keywords and error codes
- [ ] 6. Test with real devices and the Console logs
1. Sender choice
| Use case | Sender |
|---|---|
| US A2P (app notifications, alerts, marketing) moderate volume | 10DLC long code with registered brand + campaign |
| US, quick start, one brand, moderate throughput | Toll-free number with toll-free verification |
| US high volume or marketing at scale | Short code |
| OTP / login codes | Verify API (handles channels, rate limits, fraud guard) |
| WhatsApp sender via Twilio; templates for business-initiated messages | |
| Outside US | Check country rules: alphanumeric sender IDs, pre-registration, content limits |
Unregistered US 10DLC traffic is blocked (error 30034). Registration takes days to weeks; start it before writing code.
3. Sending
Node (twilio v5):
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 sendSms(userId: string, to: string, body: string) {
if (await optOuts.has(to)) return { skipped: "opted_out" };
const msg = await client.messages.create({
to, // E.164, e.g. +14155550123
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!,
body,
statusCallback: `${process.env.PUBLIC_URL}/twilio/status`,
});
await db.messages.insert({ sid: msg.sid, userId, to, status: msg.status });
return { sid: msg.sid };
}
Python:
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"])
msg = client.messages.create(
to=to, messaging_service_sid=os.environ["TWILIO_MESSAGING_SERVICE_SID"],
body=body, status_callback=f"{os.environ['PUBLIC_URL']}/twilio/status",
)
Sending rules:
- Normalize numbers to E.164 at input (libphonenumber); store normalized.
- Twilio has no idempotency key on message create. Guard retries yourself: create your DB
row with a unique
(user_id, purpose, dedupe_key)first, then send, then store the SID. Retry only on network errors/5xx, never on 4xx validation errors. - Segments drive cost: GSM-7 is 160 chars (153 per segment when split); any non-GSM character (emoji, curly quotes) switches to UCS-2 at 70 (67 per segment). Enable smart encoding on the Messaging Service and keep templates ASCII-safe.
- Throughput is per sender and registration tier; the Messaging Service queues excess.
Long queues mean stale messages; set
validityPeriodfor time-sensitive texts. - Respect quiet hours for non-urgent messages (US TCPA: 8am-9pm recipient local time; some states are stricter).
4. Webhooks
Express with the SDK middleware (validates signature using the Auth Token):
import express from "express";
import twilio from "twilio";
const app = express();
app.set("trust proxy", true);
app.use("/twilio", express.urlencoded({ extended: false }));
// reads TWILIO_AUTH_TOKEN from env; protocol/host override what the app sees behind a proxy
const validate = twilio.webhook({ protocol: "https" });
app.post("/twilio/inbound", validate, async (req, res) => {
const { From, Body, MessageSid } = req.body;
await inbox.saveOnce(MessageSid, { from: From, body: Body });
const twiml = new twilio.twiml.MessagingResponse();
// reply only if needed; an empty <Response/> sends nothing
res.type("text/xml").send(twiml.toString());
});
app.post("/twilio/status", validate, async (req, res) => {
const { MessageSid, MessageStatus, ErrorCode } = req.body;
await db.messages.updateStatus(MessageSid, MessageStatus, ErrorCode ?? null);
res.sendStatus(204);
});
Manual validation (any framework):
const ok = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!,
req.header("X-Twilio-Signature") ?? "",
fullPublicUrl, // exact URL Twilio called, incl. https, host, path, query
req.body, // parsed form params
);
if (!ok) return res.sendStatus(403);
Python: RequestValidator(os.environ["TWILIO_AUTH_TOKEN"]).validate(url, request.form, signature).
Webhook rules:
- Signature mismatch behind a proxy or load balancer is almost always the URL: Twilio
signs the public
https://URL; your app seeshttp://or an internal host. Rebuild the URL fromX-Forwarded-Proto/Hostor configure the canonical public URL. - JSON-bodied webhooks (some newer products) are validated with the body hash
(
validateRequestWithBody); form-encoded SMS/voice use the param method above. - Status callbacks arrive out of order and more than once. Only move status forward:
queued < sending < sent < delivered | undelivered | failed(plusreadfor WhatsApp). - Respond fast (well under 15 seconds); queue heavy work.
5. Errors, opt-outs, keywords
| Code | Meaning | Action |
|---|---|---|
| 21211 | Invalid To number | Mark number invalid; fix input validation |
| 21610 | Recipient replied STOP | Mark opted out; never retry |
| 21614 | Not a mobile number | Mark landline; offer voice or email |
| 30003 / 30005 | Unreachable / unknown destination | Retry later once; then flag |
| 30006 | Landline or unreachable carrier | Flag number |
| 30007 | Carrier filtered (content/spam) | Review content, links, registration; do not resend same text |
| 30034 | Unregistered 10DLC traffic blocked | Finish A2P registration |
- Default Twilio opt-out handling replies to STOP/START/HELP keywords on long codes and
toll-free. Mirror opt-out state in your DB from inbound messages (
OptOutTypeparam when Advanced Opt-Out is enabled) so your app stops queueing to those numbers too. - Customize HELP text with brand name and support contact; it is a registration requirement.
WhatsApp specifics
- Business-initiated messages outside the 24-hour customer service window must use an
approved template, sent with
contentSidandcontentVariables(Content API). - Free-form replies are allowed only within 24 hours of the user’s last inbound message.
- Address format:
whatsapp:+14155550123fortoandfrom.
Verify (OTP)
await client.verify.v2.services(process.env.TWILIO_VERIFY_SID!)
.verifications.create({ to, channel: "sms" });
const check = await client.verify.v2.services(process.env.TWILIO_VERIFY_SID!)
.verificationChecks.create({ to, code });
if (check.status !== "approved") throw new Error("invalid code");
Rate-limit verification starts per user and per IP in your app as well, and enable Verify fraud protections to block SMS pumping to premium-rate prefixes.
Voice basics
- Inbound calls hit the number’s voice webhook; reply with TwiML (
VoiceResponse:say,gather,dial,record). Validate signatures exactly as for SMS. - Outbound:
client.calls.create({ to, from, url })whereurlreturns TwiML; addstatusCallbackandstatusCallbackEventfor lifecycle tracking.
Gotchas
- Trial accounts can only message verified numbers and prefix messages; test on a paid subaccount for realistic behavior.
- URL shorteners (bit.ly etc.) trigger carrier filtering; use your own branded domain.
- Message content must match the registered campaign use case; sending marketing over an “account notifications” campaign gets filtered.
- Do not log full message bodies containing OTPs or personal data.
- SMS pumping fraud: restrict Geo Permissions to countries you serve.
Verification
- Send to a real handset; status progresses to
deliveredin your DB via the callback. - POST a forged request without a valid signature to each webhook: 403.
- Text STOP from the handset: subsequent sends are skipped by your app and Twilio returns 21610 if attempted; START re-enables.
- Check Messaging Insights / logs for error codes over the first week of traffic.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/compliance.md
# Messaging compliance (US/Canada focus)
Read this before launching a new SMS program, adding marketing messages, or when carrier
filtering (30007) or registration errors (30034) appear. This is operational guidance,
not legal advice; route marketing programs past counsel.
## Registration paths
| Path | What you register | Typical timeline | Notes |
|---|---|---|---|
| A2P 10DLC | Brand (business identity, EIN) + Campaign (use case, sample messages, opt-in flow) | Days to a few weeks (campaign vetting) | Throughput depends on brand trust score and campaign type; sole proprietor tier is very limited |
| Toll-free verification | Business info, use case, opt-in description, sample messages, volume | Days to weeks | Unverified toll-free traffic is blocked |
| Short code | Program brief per carrier | Weeks to months | Highest throughput; required for some high-volume marketing |
| Canada | Toll-free or short code common; follow CTIA-equivalent guidance and CASL consent rules | - | CASL requires express or implied consent with records |
Campaign submissions are rejected most often for: opt-in flow not described or not
publicly visible, sample messages missing brand name or opt-out language, website that
does not match the brand, privacy policy that allows sharing phone numbers with third
parties for marketing.
## Consent
- Collect consent with clear disclosure at the point of capture: brand, message purpose,
frequency ("up to 4 msgs/month"), "Msg & data rates may apply", "Reply STOP to cancel,
HELP for help", and links to terms and privacy policy.
- Marketing messages need express written consent (checkbox unticked by default, not
bundled as a condition of purchase). Transactional messages need the user to have
provided the number for that purpose.
- Store per number: timestamp, source (form URL / keyword / paper), consent text version,
IP or device where available.
- Send a confirmation message on opt-in that repeats brand, frequency, and STOP/HELP.
## Keywords
| Type | Keywords (case-insensitive, whole message) | Required behavior |
|---|---|---|
| Opt-out | STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT (carriers and Twilio have expanded this list, e.g. OPTOUT, REVOKE) | Stop all messages for that program immediately; one confirmation reply allowed |
| Opt-in | START, UNSTOP, YES | Resume; send confirmation |
| Help | HELP, INFO | Reply with brand, support contact, and opt-out instructions |
- Twilio's default handling covers these on long codes and toll-free; Advanced Opt-Out on a
Messaging Service lets you customize replies and keywords. Short codes need your own
handling.
- Treat clear natural-language opt-outs ("please stop texting me") as opt-outs too; the
FCC's 2025 revocation rules require honoring any reasonable means of revoking consent.
- Sync opt-out state to your DB so other systems (marketing tools, CRM) also stop.
## Content rules that reduce filtering
- Include the brand name in the first message of a conversation and in templates.
- Avoid public URL shorteners; use a branded domain with HTTPS.
- No SHAFT content (sex, hate, alcohol, firearms, tobacco) without the right campaign type
and age gating; cannabis and some financial offers (loans, debt relief) are restricted
or disallowed.
- Keep content consistent with the registered use case and sample messages.
- Avoid ALL CAPS, excessive punctuation, and identical high-volume blasts from many
numbers ("snowshoeing").
## Timing
- TCPA quiet hours: no marketing before 8am or after 9pm recipient local time; several
states (for example Florida, Oklahoma) set narrower windows and frequency caps.
Derive local time from the recipient's address or area code as a fallback.
- Transactional alerts the user requested (OTP, delivery updates) may send anytime, but
batch non-urgent ones into daytime.
## WhatsApp policy points
- Users must opt in to receive WhatsApp messages from the business.
- Templates are categorized (marketing, utility, authentication) and priced differently;
miscategorized templates get re-categorized or rejected.
- Quality rating drops with blocks/reports; low quality reduces messaging limits.
## Pre-launch checklist
- [ ] Registration approved for every sender in the Messaging Service
- [ ] Opt-in UI shows required disclosures; consent records stored
- [ ] Confirmation message on opt-in includes brand, frequency, STOP/HELP
- [ ] STOP/START/HELP tested from a real handset; DB opt-out state updates
- [ ] Sample production messages match the registered campaign
- [ ] Quiet hours enforced for marketing sends
- [ ] Geo Permissions limited to served countries
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.