SMS STOP keyword immediately removes user from marketing SMS lists
Why it matters
TCPA §227(b) imposes strict liability for sending SMS to numbers on the national Do-Not-Call registry or after a STOP reply — penalties run $500–$1,500 per message. Most SMS providers (Twilio, Vonage) auto-honor STOP at the carrier level, but that protection evaporates the moment you switch providers or add a second sending path. Without an inbound webhook that writes opt-out status to your database, the only suppression record lives in a third-party system you don't control. GDPR Art. 7(3) and CCPA §1798.120 add parallel requirements: withdrawal of consent must be honored immediately and must propagate to all processing activities.
Severity rationale
High because relying solely on carrier-level STOP handling leaves opted-out numbers unprotected if the provider changes or a second SMS path is added — each message sent to an opted-out number is an independent TCPA violation with statutory damages.
Remediation
Configure an inbound Twilio webhook that validates the signature and writes smsOptOut: true synchronously, then check that flag in every outbound send.
// app/api/webhooks/sms/route.ts
const STOP_KEYWORDS = ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT']
export async function POST(req: Request) {
const body = await req.formData()
const params = Object.fromEntries(body.entries()) as Record<string, string>
const sig = req.headers.get('x-twilio-signature') ?? ''
if (!validateRequest(process.env.TWILIO_AUTH_TOKEN!, sig, process.env.TWILIO_WEBHOOK_URL!, params)) {
return new Response('Forbidden', { status: 403 })
}
if (STOP_KEYWORDS.includes((params['Body'] ?? '').trim().toUpperCase())) {
await db.smsSubscriber.updateMany({
where: { phoneNumber: params['From'] },
data: { smsOptOut: true, optOutAt: new Date() },
})
}
return new Response('<Response></Response>', { headers: { 'Content-Type': 'text/xml' } })
}
Also guard every outbound send with a smsOptOut: false check before calling the provider API.
Detection
-
ID:
sms-stop-honored -
Severity:
high -
What to look for: Enumerate every relevant item. TCPA and CTIA guidelines require that SMS senders honor STOP (and equivalent keywords: STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT) immediately. Most SMS service providers (Twilio, Vonage, Plivo) automatically handle STOP at the carrier/platform level and block further sends to that number. However, the application must also update its own database to reflect the opt-out — otherwise, if you switch providers or send through a different channel, you will re-message an opted-out user. Check whether there is an inbound SMS webhook handler that processes STOP replies. Check whether the handler updates the subscriber's opt-out status in the database. Check whether the
sms_opt_inor equivalent field is checked before every outbound SMS send. -
Pass criteria: At least 1 of the following conditions is met. An inbound SMS webhook handler exists that receives carrier opt-out notifications (or directly handles STOP replies) and immediately updates the subscriber's SMS opt-out status in the database. The outbound SMS send logic checks this status and never sends marketing SMS to opted-out numbers. If the SMS provider handles STOP automatically, the application additionally syncs this status to its own database.
-
Fail criteria: No inbound SMS webhook configured. STOP is handled by the SMS provider but the application database is never updated, creating a gap if the sending channel changes. Outbound SMS send does not check local opt-out status.
-
Do NOT pass when: The item exists only as a placeholder, stub, or TODO comment — partial implementation does not count as passing.
-
Skip (N/A) when: The application sends no marketing or promotional SMS — only transactional notifications (e.g., OTP codes, appointment reminders triggered by user action).
-
Cross-reference: For user-facing accessibility and compliance, the Accessibility Basics audit covers foundational requirements.
-
Detail on fail: Example:
"Twilio is configured for outbound SMS but no inbound webhook handler found in codebase. STOP handling is entirely delegated to Twilio; local database never updated. If sending channel changes, opted-out users will be re-messaged."or"No SMS opt-out status field in database. STOP replies not processed.". -
Remediation: Configure an inbound webhook and sync opt-out status to your database:
// app/api/webhooks/sms/route.ts — Twilio inbound SMS webhook import { validateRequest } from 'twilio' export async function POST(req: Request) { // Verify the request came from Twilio const url = process.env.TWILIO_WEBHOOK_URL! const authToken = process.env.TWILIO_AUTH_TOKEN! const sig = req.headers.get('x-twilio-signature') ?? '' const body = await req.formData() const params = Object.fromEntries(body.entries()) as Record<string, string> if (!validateRequest(authToken, sig, url, params)) { return new Response('Forbidden', { status: 403 }) } const from = params['From'] ?? '' const messageBody = (params['Body'] ?? '').trim().toUpperCase() const stopKeywords = ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT'] if (stopKeywords.includes(messageBody)) { // Immediately mark the number as opted out await db.smsSubscriber.updateMany({ where: { phoneNumber: from }, data: { smsOptOut: true, optOutAt: new Date(), optOutKeyword: messageBody }, }) } // Twilio expects a TwiML response return new Response('<Response></Response>', { headers: { 'Content-Type': 'text/xml' }, }) }// In your outbound SMS sending function — always check opt-out status export async function sendMarketingSms(to: string, body: string) { const subscriber = await db.smsSubscriber.findFirst({ where: { phoneNumber: to } }) if (!subscriber || subscriber.smsOptOut) { console.warn(`[sms] Skipping opted-out number: ${to}`) return } // Proceed with send }
External references
- external · TCPA-§227(b) — TCPA 47 U.S.C. §227(b) — SMS opt-out / STOP keyword requirements
- gdpr · Art. 7(3) — GDPR Art. 7(3) — Withdrawal of consent
- ccpa · §1798.120 — CCPA §1798.120 — Right to opt out
Taxons
History
- 2026-04-18·v1.0.0·Initial import from email-sms-compliance·automated