You wired up Twilio. Your test messages went through. You shipped. Then a week later, delivery rates cratered and your logs filled with error 30034, and now you are reading a PDF from a mobile carrier that uses the phrase "messaging ecosystem integrity" without irony.

This post is the explanation I wanted when that happened to me. No compliance-speak. Just what the system is, why your messages are failing, and the architectural decision that makes most of the pain go away.

What A2P 10DLC actually is

A2P means application-to-person: a machine sending a message to a human. 10DLC means 10-digit long code, which is a normal-looking phone number as opposed to a short code (like 55555) or a toll-free number.

For years, developers used regular phone numbers to send application traffic because it was cheap and nobody stopped them. Spammers noticed the same thing. US carriers responded by building a registration system: if you want to send application traffic over a 10-digit number, you register who you are and what you are sending, and unregistered traffic gets filtered or blocked outright.

That is the whole thing. It is not a law. It is a set of carrier policies enforced through a shared registry called The Campaign Registry.

The practical consequence: your unregistered number is not "working but slow." It is on a path to being blocked entirely, and carriers have gotten progressively less patient about it.

The registration hierarchy

Three objects, nested.

Brand is you, the legal entity. EIN or equivalent tax ID, legal business name, address, website. This produces a trust score.

Campaign is a specific use case attached to a brand. Two-factor authentication, appointment reminders, marketing, customer care, and so on. One brand can run several campaigns. Each is reviewed independently.

Number is the phone number assigned to a campaign. A campaign can hold multiple numbers.

The piece that surprises people is the trust score. It is assigned to your brand, not your campaign, and it directly controls your throughput. A low-score brand might get 75 message segments per minute across all its numbers. A vetted, high-score brand gets thousands. Registering a second number does not increase your ceiling, because the ceiling belongs to the brand.

Sole proprietors have their own track with much lower limits, typically a single number and a few thousand segments per day. If you are pre-incorporation, this is the lane you are in, and it is worth knowing before you architect anything that assumes volume.

The distinction that changes everything

Carriers categorize traffic broadly along a conversational-to-promotional axis, and the compliance burden is wildly different at each end.

Promotional traffic is you initiating contact. Sending offers, announcements, campaigns, anything blasted to a list. This requires documented consent for every recipient, gets the heaviest scrutiny during campaign review, has the strictest content filtering, and is where most rejections happen.

Conversational traffic is a reply. The user texted you, or the user took an action in your system that explicitly requested a message, and your message is a direct response to that. Consent is implicit in the initiating action. Review is lighter. Filtering is more forgiving.

Most developers architect their SMS feature as a broadcast system with a recipient list, then spend weeks fighting registration. If your product can be built so that every outbound message is triggered by an inbound event, you have moved yourself into the easier category by design rather than by paperwork.

Inbound-triggered architecture

Here is the pattern concretely. Instead of maintaining a list and pushing to it, you make every message a response.

The user calls your number, or fills a form, or replies to a message. That inbound event is the trigger. Your system responds. Every outbound message in your logs traces back to an inbound action with a timestamp.

// Webhook receives inbound event (call completed, form submitted, SMS received)
app.post('/webhook/inbound', async (req, res) => {
  const { from, eventType, callSid } = req.body;

  // The consent record IS the inbound event. Store it.
  const consentRecord = await db.consent.create({
    phone: from,
    source: eventType,          // 'inbound_call' | 'inbound_sms' | 'web_form'
    reference: callSid,
    timestamp: new Date(),
    ip: req.ip || null
  });

  // Outbound is now a reply, not a broadcast
  await twilio.messages.create({
    to: from,
    from: process.env.REGISTERED_NUMBER,
    body: buildResponse(eventType),
    statusCallback: 'https://yourapp.com/webhook/status'
  });

  res.sendStatus(200);
});

Enter fullscreen mode Exit fullscreen mode

Two things this buys you beyond category classification.

First, your consent record is generated automatically as a byproduct of normal operation. When a carrier or your provider asks how you obtained consent for a given number, you have a timestamped inbound event with a call SID or form submission attached. You are not reconstructing anything from a spreadsheet.

Second, your opt-out handling gets simple, because you are already listening on the inbound path.

const STOP_KEYWORDS = ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT'];
const HELP_KEYWORDS = ['HELP', 'INFO'];

async function handleInboundSms(from, body) {
  const normalized = body.trim().toUpperCase();

  if (STOP_KEYWORDS.includes(normalized)) {
    await db.optOut.upsert({ phone: from, optedOutAt: new Date() });
    return null; // Carrier auto-replies to STOP. Do not send your own.
  }

  if (HELP_KEYWORDS.includes(normalized)) {
    return `${BRAND_NAME}: support at ${SUPPORT_EMAIL}. Reply STOP to opt out. Msg&data rates may apply.`;
  }

  const optedOut = await db.optOut.findUnique({ where: { phone: from } });
  if (optedOut) return null;

  return await generateReply(from, body);
}

Enter fullscreen mode Exit fullscreen mode

Note the STOP branch returns null. Carriers intercept STOP and send their own confirmation. If you also send one, you have just messaged someone who opted out, and that lands on you.

Provider comparison

The registration process is standardized through The Campaign Registry, so the differences are in tooling, fees, and how much of the process the provider absorbs.

Twilio has the most polished registration UI and the best documentation. It also has the most conservative filtering, so borderline content gets blocked sooner. Registration fees are passed through with a markup. If you want the process to be as guided as possible and you can absorb slightly higher per-message costs, this is the default.

Telnyx is meaningfully cheaper per segment and per registration, with a more developer-oriented API surface. The onboarding is less hand-held, and the support experience assumes you know what you are doing. Good fit once you understand the system.

Plivo sits between the two on both price and tooling. Strong international coverage if you have traffic outside the US, which matters because 10DLC is a US-only regime and your architecture may need to branch by country.

For all three, brand registration takes minutes to hours. Campaign approval typically takes one to several business days and is where rejections happen.

The gotchas

Campaign rejections are usually about your website, not your code. Reviewers check that your site describes the messaging use case, shows how consent is collected, and has a privacy policy stating you do not sell phone numbers. A campaign describing appointment reminders when your homepage says nothing about appointments gets rejected. Fix the site before resubmitting.

Your sample messages must match what you actually send. Submit real message templates with variables clearly marked. Generic placeholders read as evasive.

HELP is mandatory and STOP is enforced. Every campaign needs a working HELP response with your brand name and support contact. Failure to honor opt-outs is the fastest route to having a campaign killed.

Segments, not messages. Throughput limits count segments. A message over 160 GSM-7 characters splits into multiple segments. One emoji forces the entire message into UCS-2 encoding, dropping the segment boundary to 70 characters. A cheerful 200-character message with a single emoji is four segments against your quota.

Sole proprietor limits are real. If you registered as an individual, you have a hard ceiling that no amount of optimization gets around. Plan for incorporation before you plan for scale.

Toll-free is a different regime. Toll-free numbers do not use 10DLC registration. They have their own verification process, often faster to approve, with different throughput characteristics. Worth evaluating if 10DLC registration is blocking you and your use case fits.

The short version

10DLC is not a tax you pay after building. It is a constraint that should shape the design.

If every outbound message in your system is a reply to something the user did, you get easier classification, automatic consent records, simpler opt-out handling, and a campaign application that reviewers approve without a fight. If your system is a broadcast engine with a list, you will fight the registry, and you will keep fighting it every time you add a use case.

Build it as a conversation. The compliance mostly takes care of itself.


I build VoiceIntego, an AI voice receptionist for dental and home service businesses, which is where the inbound-triggered pattern above comes from.