⚙️ We're performing scheduled maintenance — new sign-ups are temporarily paused. Existing customers can still log in here. We'll be back shortly.
Back to blog

compliance

SMS Compliance for Developers: A Practical Guide (TCPA + CTIA)

Production-ready SMS compliance for developers. Opt-out keywords, quiet hours, consent logging, 10DLC, and code examples for every requirement.

Noa

SMS Compliance for Developers: A Practical Guide (TCPA + CTIA)

Most SMS compliance guides read like legal memos: long lists of statutes, no code, no engineering guidance. By the time you finish reading, you know what the law says but not what your application must do at the API call.

This guide is the opposite. Every requirement maps to a code pattern your messaging stack has to implement. If your platform does not enforce these in code — at the moment a message is queued — you are one customer complaint away from a TCPA suit that pays out $500 to $1,500 per message. There is no statutory cap. A single bad campaign can become a seven-figure liability.

This post covers the two regulatory frameworks (TCPA and CTIA), each compliance requirement they create, and the code pattern that satisfies each one. Examples reference the actual implementation in the senderZ codebase so you can see what production enforcement looks like.

Why SMS Compliance Is an Engineering Problem

Three financial realities make compliance a code problem, not a legal review.

TCPA penalties are per-message. The Telephone Consumer Protection Act allows $500 in statutory damages per offending message. If a court finds the violation was willful or knowing, that triples to $1,500. There is no cap. A campaign that sends 10,000 messages to a list with even partial consent issues can produce $5,000,000 in liability before legal fees.

Carriers filter silently. CTIA messaging guidelines are not law — they are industry standards enforced by AT&T, T-Mobile, Verizon, and the carrier registrar databases. When you violate them, the carrier does not return an error code. The message is silently filtered. Your application thinks it succeeded. Your customer never sees the text. Carrier filtering produces no audit trail your code can react to.

Compliance happens at the API call. Consent checks, opt-out enforcement, quiet-hour gating, content scanning — these all run in the milliseconds between your application calling POST /v1/messages and the message actually being dispatched to a carrier. If your messaging platform does not enforce them automatically, your application has to. The compliance burden lives in the request path, not in a quarterly legal review.

The right framing: compliance is the same kind of infrastructure concern as rate limiting, observability, and security. If it is not enforced in code, it is not enforced.

For more on senderZ’s approach to compliance, see the trust center page on compliance.

The Two Frameworks You Must Know

Two regulatory layers govern A2P SMS in the United States. They overlap but enforce differently.

TCPA (Telephone Consumer Protection Act). Federal law, enforced by the FCC and through a private right of action. Any individual who receives a non-compliant message can sue and collect $500 per message ($1,500 if willful). Class actions are common. State attorneys general can also bring enforcement actions.

CTIA Messaging Principles and Best Practices. Industry-published guidelines that mobile carriers enforce by filtering or blocking traffic. Not law, but operationally indistinguishable: a message that does not reach the recipient is a message that did not work.

A few additional layers worth knowing:

  • State-level mini-TCPAs. Florida (FTSA), California (CCPA-adjacent texting rules), Virginia (10-year consent retention), Washington (CEMA). Some state laws are stricter than federal. Apply the strictest rule for the recipient’s residence.
  • The Campaign Registry (TCR). Since February 2025, A2P SMS over 10-digit long codes must register with TCR. Unregistered traffic is blocked at the carrier level. See our 10DLC explainer for the registration walkthrough.
  • iMessage exception. iMessage routes through Apple’s push notification infrastructure, not the cellular network. CTIA rules do not apply. TCPA still does — TCPA covers any form of automated text messaging, regardless of channel. But carriers cannot filter iMessage because they never see it.

The TCPA + CTIA combination means that even a fully consented contact list can produce zero deliverability if your stack misbehaves at the technical layer. Both legal compliance AND operational compliance have to be solved.

For the underlying federal rules, see the FCC’s TCPA consumer guidance and the CTIA Messaging Principles.

Opt-Out Handling (STOP Keyword)

The single most important compliance requirement. Every SMS platform must process opt-out keywords on every inbound message and block all future sends to that number.

Required keywords

The CTIA mandates that the following keywords trigger opt-out, regardless of case or surrounding punctuation:

STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT

The following keywords trigger opt-in (re-subscription):

START, UNSTOP, YES

Match case-insensitively. Trim whitespace. Do not require an exact match against the whole message body — many users send Please STOP or STOP texting me, and that still counts as an opt-out.

Processing timeline

The legal text says “within 10 business days.” The carrier-enforced reality is “immediately.” Carriers expect opt-out processing to happen before the next outbound message to that number. If you process within 24 hours, you are within both the legal window and the operational expectation.

Required code pattern

You need a table that stores opt-outs, scoped per tenant if you are running multi-tenant:

CREATE TABLE opt_outs (
  id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL,
  phone_number TEXT NOT NULL,
  opted_out_at TEXT NOT NULL,
  opted_back_in_at TEXT,
  UNIQUE(tenant_id, phone_number)
);

Then, before every outbound send:

async function checkOptOut(
  tenantId: string,
  toNumber: string,
  db: D1Database,
): Promise<boolean> {
  const row = await db
    .prepare(
      'SELECT id FROM opt_outs WHERE tenant_id = ? AND phone_number = ? AND opted_back_in_at IS NULL LIMIT 1',
    )
    .bind(tenantId, toNumber)
    .first()
  return row !== null
}

if (await checkOptOut(message.tenant_id, message.to, db)) {
  await markMessageBlocked(message.id, 'opted_out')
  return
}

The check must happen at the router layer, before any external API call. If you check at the application layer and the application has a bug, you fall back to the messaging platform — which may or may not enforce. Defense in depth means the check is closest to the actual send.

Confirmation reply

When you process a STOP, send exactly one confirmation reply. Then nothing. Sending two confirmations or any follow-up “marketing” message after an opt-out is itself a TCPA violation. A safe confirmation text:

You have been unsubscribed and will receive no further messages from <brand>.

senderZ enforces opt-out checks before any outbound dispatch — so customers using the senderZ API never have to implement this themselves. For the customer-side opt-out export endpoint, see the opt-outs documentation. The Tier 2 companion post STOP texts and opt-outs covers the same topic for non-developer audiences.

Quiet Hours (Marketing Only)

The TCPA implementing regulations restrict marketing messages to local-time business hours. The carrier-enforced standard is 8:00 AM to 8:00 PM in the recipient’s time zone.

What counts as marketing

Most legal advice draws a distinction between transactional (OTP codes, appointment reminders, shipment notifications) and marketing (promotions, newsletters, sale announcements). Quiet-hour rules apply only to marketing. Transactional messages can ship at any hour.

The line is sometimes blurry. A general rule of thumb: if the recipient explicitly asked for this specific message (“here is the verification code you requested”), it is transactional. If you decided to send the message based on a campaign trigger, it is marketing.

Required pattern: reschedule, do not block

When a marketing message arrives at the router during the recipient’s quiet hours, do not silently drop it or return an error. Reschedule for 8:00 AM local. Most messaging platforms support delayed delivery via a queue or scheduled-send API.

async function applyQuietHours(message: OutboundMessage): Promise<'send' | 'defer'> {
  if (message.type !== 'marketing') return 'send'

  const recipientTz = inferTimezone(message.to)
  const localHour = getHourInTimezone(new Date(), recipientTz)

  if (localHour >= 20 || localHour < 8) {
    const wakeAt = nextLocalHour(8, recipientTz)
    await scheduleDelayedSend(message, wakeAt)
    return 'defer'
  }

  return 'send'
}

Inferring time zone

US phone numbers carry their area code, which is a strong (but imperfect) signal for the user’s home time zone. Maintain a map of area code to IANA time zone (212 -> America/New_York, 415 -> America/Los_Angeles, etc.) and fall back to Eastern for unknowns.

For higher precision, log the recipient’s stated time zone at consent capture if you can — but area code is a reasonable default for US senders.

senderZ implements quiet-hour logic at the router layer and uses delayed-delivery to reschedule. Customer code does not opt into this — it is on by default for any message marked as marketing.

If you ever face a TCPA suit, the case turns on consent records. Plaintiffs claim they never opted in. Defendants must produce timestamped, verifiable proof that they did. Bad recordkeeping loses cases that strong opt-in flows would otherwise win.

What to log

Every consent action should record:

  • Phone number (normalized to E.164)
  • Tenant ID (for multi-tenant systems)
  • Consent typeexpress_written (highest), express (explicit but unsigned), or implied (existing business relationship)
  • Consent sourceweb_form, sms_keyword, api, paper_signup, etc.
  • Consented at — ISO 8601 timestamp
  • IP address — if captured at web signup
  • Anything else available — user agent, form URL, the actual checkbox text the user saw

Schema

CREATE TABLE consent_log (
  id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL,
  phone_number TEXT NOT NULL,
  consent_type TEXT NOT NULL,
  consent_source TEXT,
  consented_at TEXT NOT NULL,
  ip_address TEXT,
  user_agent TEXT,
  created_at TEXT NOT NULL
);

CREATE INDEX idx_consent_phone ON consent_log(tenant_id, phone_number);

Retention

Federal TCPA case law generally requires four years of consent records. Virginia (Va. Code section 59.1-518) requires ten years. If you have any Virginia recipients, retain for ten years to be safe — there is no operational reason to delete consent records sooner, and the storage cost is negligible.

Immutability

Never update a consent record. If a user re-consents, write a new row. If they revoke, write a separate opt-out record. Mutating consent rows undermines the evidentiary value of the log: a defendant who can show “we never modified this record after writing it” is harder to attack.

senderZ exposes a POST /v1/compliance/consent endpoint for customers to log consent at their signup forms. See the consent documentation. The records are append-only and exportable as CSV from the operator portal for legal defense purposes.

10DLC Registration (For Real A2P SMS)

If you send Application-to-Person SMS over standard 10-digit US long codes, you must register with The Campaign Registry. Since February 2025, unregistered A2P traffic is blocked by carriers — not throttled, not slow, completely blocked.

Two-step process

  1. Brand registration. Register your business entity with TCR. Costs around $44 one-time. Approval is usually same-day if your records match TIN data.
  2. Campaign registration. Register each use case (marketing, transactional, OTP, etc.) under your brand. Costs $10 to $15 per campaign per month plus a one-time fee. Approval takes 1 to 7 business days.

Once approved, you receive a campaign ID that your messaging platform uses when routing through 10DLC carriers.

Throughput tiers

Registered campaigns get throughput allocations:

  • Standard — about 10 messages per second per number
  • Charity / political — capped differently
  • Top-tier (high-volume marketing) — requires brand vetting, higher throughput

The iMessage carve-out

iMessage does not route over the cellular A2P network. It routes through Apple’s push infrastructure. Carriers cannot see iMessage traffic, cannot filter it, and cannot impose 10DLC registration on it. iMessage senders do not need to register with TCR.

This matters for hybrid platforms. If your default channel is iMessage with SMS fallback, only your SMS volume is subject to 10DLC. Low-volume SMS fallback (say, the 30 percent of US users on Android) often stays under thresholds where individual carrier registration becomes mandatory.

For more on 10DLC and how to avoid the registration requirement entirely on iMessage-only flows, see what is 10DLC.

The Campaign Registry is the authoritative source: campaignregistry.com.

Daily Send Limits and Carrier Filtering

Even fully registered, fully consented traffic gets filtered if it exceeds CTIA P2P (person-to-person) thresholds. The carriers’ goal is to detect commercial spam masquerading as personal messaging. The thresholds are not published as hard limits, but the operational ranges are well understood:

  • About 1,000 outbound messages per number per day before A2P-style filtering kicks in
  • About 15 outbound messages per minute per number as a short-window rate
  • Roughly 1:1 inbound to outbound ratio over rolling windows — heavily outbound numbers get flagged

When a number is flagged, messages get filtered silently. The sending platform returns success; recipients never see the message.

Mitigation: pool routing

The right pattern at scale is to route across a pool of phone numbers. Each number stays under the per-number limit. When one number approaches the daily cap, the router falls back to another.

async function pickPhone(
  tenantId: string,
  db: D1Database,
): Promise<Phone | null> {
  const phone = await db
    .prepare(
      `SELECT * FROM phones
       WHERE tenant_id = ?
         AND status = 'active'
         AND messages_today < daily_limit
       ORDER BY messages_today ASC
       LIMIT 1`,
    )
    .bind(tenantId)
    .first<Phone>()
  return phone
}

The query picks the least-used active phone. If every phone is at capacity, return null and the router fails the message rather than risk filtering.

senderZ implements this in the router layer. Customers on pooled plans get this automatically. Customers on Growth or Scale plans with dedicated numbers can configure their own pool limits.

SHAFT Content Rules

CTIA-defined restricted categories. Messages whose content falls under these categories are subject to additional carrier scrutiny and often outright blocked on standard registration:

  • Sex — adult content, escort services
  • Hate — discriminatory language, extremist content
  • Alcohol — promotion or sale of alcoholic beverages
  • Firearms — sale or promotion of weapons
  • Tobacco — includes vaping, CBD, hemp products

If your product touches any of these categories, you have two options. One: register your campaign at the appropriate tier (carriers do permit age-gated alcohol marketing with the right registration). Two: route through channels that bypass carrier filtering (iMessage does not have SHAFT enforcement, though Apple’s acceptable use policy has its own content rules).

The most common surprise is CBD. CBD products are federally legal but classified under the tobacco SHAFT category. Standard 10DLC campaigns will get filtered. You need a specific cannabis/CBD campaign type and additional documentation.

Compliance as Code — A Production Checklist

If your messaging stack does not enforce these in code, you have a bug. Treat this as a deployment-blocking checklist:

  • Opt-out check before every outbound message. Per-tenant scope. Database lookup, not in-memory cache.
  • STOP/START keyword processing on every inbound message. Case-insensitive. Whitespace-tolerant. Confirmation reply only once.
  • Quiet hours enforcement for marketing messages. Reschedule, do not block. Recipient time zone, not server time zone.
  • Daily per-number send limit. Pool routing fallback when a number hits its cap.
  • Per-minute rate limit per number. Short-window throttle separate from daily limit.
  • Consent log API. Timestamped, append-only, multi-source.
  • Opt-out list export. Customer-accessible. CSV format. For audit defense.
  • 10DLC campaign ID propagation. Outbound SMS over registered campaigns must include the campaign ID.

These are the eight items senderZ enforces in production. If you build a messaging stack from scratch, this is the minimum surface area.

For the full senderZ compliance enforcement documentation, see the compliance docs and the trust center page.

FAQ

Does iMessage need TCPA compliance?

Yes. TCPA covers any form of automated text messaging regardless of delivery channel. iMessage bypasses carrier-side CTIA enforcement but is fully subject to TCPA: consent, opt-out handling, and quiet hours apply just as they do to SMS. The carrier-side rules (10DLC registration, SHAFT content filtering) do not apply because carriers do not see iMessage traffic.

What if a user opts out then re-subscribes via a different channel?

Process the re-subscription only if it explicitly references the original brand and gives clear consent. A user who signs up at a different web form weeks later has given new consent and the opt-out is no longer in effect. A user who texts START to the brand they opted out of is also re-subscribed. A user who happens to receive a message because a friend forwarded their number to your contact list has not consented and you cannot send.

Are transactional messages exempt from quiet hours?

Yes for carrier-enforced quiet hours under CTIA guidelines. OTP codes, appointment reminders, shipment updates, and similar transactional content can be delivered at any hour. The legal definition of “transactional” is narrow — it must be triggered by an action the recipient took. Anything that looks like a marketing message dressed as transactional will be flagged.

Do I need 10DLC for B2B messaging?

Yes if you are sending over 10-digit long codes to mobile phones. The recipient being a business contact does not exempt the message from carrier enforcement. The carrier cannot tell who is on the other end of a phone number. If you are messaging exclusively over iMessage, 10DLC does not apply because carriers do not handle iMessage traffic.

Federal TCPA case law generally expects at least four years. Virginia requires ten years. If you have any Virginia recipients, retain for ten years across the board. There is no operational reason to delete consent records sooner — storage cost is negligible and shorter retention only weakens your audit defense.

What is the difference between TCPA and CTIA enforcement?

TCPA is federal law enforced by the FCC and through private lawsuits. Penalties are $500 to $1,500 per message with no cap. CTIA is an industry guideline enforced by carriers (AT&T, T-Mobile, Verizon) via traffic filtering. CTIA violations do not produce lawsuits, but they do produce silent message drops, which can be operationally worse than a single TCPA suit — your customers stop receiving anything you send.


Building a production messaging platform that handles compliance correctly is most of the engineering work in this space. senderZ handles all of the above on behalf of customers, so you focus on your application and not on writing your own opt-out router. Start a 14-day free trial — no credit card required. The quickstart guide sends your first compliant message in under five minutes.

Tagged compliance tcpa ctia sms 10dlc developer

Ready to start sending?

Create your free account and send your first message in minutes.