TLDR: If you let Twilio's Messaging Service auto-handle STOP (you should), your submitted sample messages must NOT include a STOP footer. Your app never sends one. The carrier vetter will catch it. Here's the full story.
The App
I built a live birth notification app — a Next.js app where a family posts live labor updates and Twilio fans them out via SMS to a guest list.
Beautiful, purpose-built, the kind of thing that only gets made once. Family first, but built to hand off.
Why Twilio (Not Telnyx)
Apollo pushed for Telnyx. I overruled it.
My reason: Twilio's turnkey STOP/HELP/START handling via Messaging Services. When someone texts STOP, Twilio intercepts it automatically — no carrier compliance logic I have to own. Higher per-message cost, fine. I was buying peace of mind.
That decision is central to what broke later.
Wiring Phase 4
The SMS engine (sendBroadcast) does three things:
- Status callbacks — every outgoing
create()call passes astatusCallbackURL. Twilio hits it as the message moves through delivered/failed/undelivered. Signature-validated. DB updated. - STOP/START sync — inbound webhook. When Twilio fires on an inbound STOP or START, I flip
opted_outin the DB. The broadcast fanout skips opted-out recipients before it even dials Twilio. - Rate control — 20 messages/hr per event, concurrency-8 fan-out,
maxDuration=60on the serverless function so a large guest list doesn't timeout mid-broadcast.
That all shipped clean. Then I hit the 10DLC wall.
Three Rejection Codes, One Submit
Brand registered near-instant (Low Volume Standard, done in about a minute). Campaign came back with three errors simultaneously.
30886 — campaign description too generic. Easy fix: name who sends, who receives, why, and explicitly state it's not marketing.
30893 — sample messages didn't match real outbound.
This is the one that taught me something.
Because I chose Messaging Services for the turnkey STOP handling, my app never sends a "Reply STOP to unsubscribe" footer. Twilio owns that. But my submitted sample included one — habit from writing SMS copy.
Mismatch. Carriers want samples byte-for-byte identical to what actually goes out.
The second part of 30893: I opened my sample with Baby [Family Name]:. A personal name. That reads peer-to-peer to a carrier vetter. A2P messages need to open with the registered program name, not a family name.
30908 — privacy policy can't be verified.
My privacy page was live. The wording was compliant. So what was wrong?
The consent flow was behind a per-event token and password. The vetter couldn't reach the signup form — couldn't verify consent OR the privacy policy at the point of opt-in.
The fix wasn't rewording the policy. It was building a public /messaging program page — no login, describes the program, shows the opt-in mechanism, links to privacy and terms. Consent has to be visible to a stranger. That's the test.
Where It Stands
Resubmitted June 19. Carrier review queue runs 5–15 days. Still waiting.
Until the campaign clears, the Twilio credentials stay unset in production. An approved-but-inactive configuration would attempt filtered sends. Not worth it.
Why This Stuck With Me
I picked Twilio specifically because it handles STOP for me. Then that same choice — combined with old copywriting habits — got my samples rejected.
The lesson isn't "don't trust Twilio's STOP handling." It's know exactly what your app sends, and submit that. Nothing more. Nothing less.
And make your consent flow reachable by anyone. The carrier vetter is a stranger. If a stranger can't find where your users opted in, you haven't proven consent.
P.S. The app itself — the reason any of this exists — is to share a baby's arrival with the people who love that family. The compliance machinery nearly blocked it. Worth every hour to get it right.