TLDR: US A2P SMS is carrier-blocked without 10DLC registration. Twilio's campaign wizard has three silent failure modes — here's every one, plus the CDP trick for the nastiest.
the setup
I built a birth announcement SMS app — an app that lets a new parent broadcast a birth announcement via SMS to a gated family feed.
Cute idea, right?
Except the moment you try to deliver US-bound application texts, the carriers block you by default. Twilio calls this A2P 10DLC (Application-to-Person, 10-digit long code) — a mandatory carrier registration system. No registration, no delivery. Just silence.
So. Had to register.
the wall
Brand registration felt easy. "Registered" came back in about a minute. I assumed campaign approval would go the same way.
It didn't.
Campaign rejected.
No real reason in the rejection itself. I had to go hunting.
what I tried that didn't work
First — I couldn't even get back to the campaign. I pasted the detail URL into Arc (my browser) directly. Instant 403. The Twilio console is an SPA (single-page app — it routes client-side, not via traditional page loads), so hard-navigating to a campaign URL just… breaks. You have to reach it through the left-nav: Messaging → Regulatory Compliance → Campaigns, then click the row anchor. That is the only path that works.
Second — the edit modal wouldn't save. I needed to re-enter the privacy and terms URLs. Typed them in. Hit Update. Button stayed greyed out.
This one burned time.
The Twilio console uses React controlled inputs. I drive the browser via arc-cdp eval (Chrome DevTools Protocol — scripted browser control, no new tab). The thing is, React keeps an internal value tracker, and a plain .value = '...' + dispatching an input event is not enough — React sees no state change, and the Update button never enables.
The actual fix:
const el = document.querySelector('input[name="privacyPolicyUrl"]');
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
nativeSetter.call(el, 'https://[your-domain]/privacy');
el._valueTracker.setValue('reset_' + Math.random());
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
Every line matters. Skip any one of them and the button stays grey.
Third — the actual reject reason. The app had no privacy policy. 10DLC campaign approval requires one. I built real /privacy and /terms pages: what data is collected, how it's used, confirmed it's never sold or used for marketing. Standard content — but it has to exist before you even submit.
the fix that worked
- Built real
/privacyand/termspages — not placeholders. - Navigated to the campaign via left-nav only — never a direct URL.
- Re-entered the privacy + terms URLs with the
_valueTrackerreset so React actually registered the change. - Resubmitted.
Campaign approved.
why this matters to me
If you're building any US SMS feature — birth announcements, alerts, reminders, anything — you will hit this wall. Twilio's docs cover the registration concept fine. They don't tell you about the SPA navigation trap, the React input trick, or the privacy-page hard requirement.
Know those three things going in and you save yourself hours of silent failure.
P.S. There's a fourth gotcha I almost buried. The phone number purchase modal has a "I agree to comply with Emergency calling terms" checkbox. If you miss it, the Buy button silently no-ops — the modal closes, no number purchased, no error message. Looked like a success. Wasn't. Check the checkbox.