Skip to main content

10DLC rejections, re-vetting, and the post-approval lifecycle

The 10DLC registration guide ends at “start sending.” This guide covers what happens next — a rejection with a cryptic code, a vetting score that caps your throughput, or an approved campaign whose daily cap no longer fits your volume. Everything here is a control on your own registrations; carrier and registry decisions remain theirs.

Where rejections come from

A rejection is one of two things, and the difference decides everything you do next:
  • Registry rejections (TCR and the CSPs that proxy it) fire during or after registration review — the filing itself is refused. These surface as FAILED / REJECTED status records with a rejectionReason string, in three places: the Settings > Compliance > 10DLC wizard, a dashboard notification on the terminal transition, and the rejectionReason field on GET /api/v1/compliance/10dlc/campaigns/:id/status.
  • Carrier rejections (AT&T, T-Mobile) can land after TCR approval, inside the per-carrier mnoStatuses map — a direct carrier audit on the brand, or one carrier disagreeing with the CSP-level decision.
Some codes are emitted before TCR decides anything: DUPLICATE-BRAND and the lowercase-usecase failure are submission-time signals, not review-phase outcomes. A rejection the wizard shows immediately after you submit usually falls in this class. Two rejections lock the record instead of requesting an amendment — BRAND-DCA-FAIL and DUPLICATE-BRAND return a fix card with resubmit_allowed: false, meaning the brand id is burned and the fix is a fresh registration, not an edit.

Decode a raw rejection code into a fix card

TCR and the CSPs return codes like 30883, 40016, and EIN-MISMATCH with a one-line free-text reason and no per-field fix. The remediation table on Troubleshooting: 10DLC campaign rejected covers the common cases statically; the decoder endpoint applies the same catalog to any code — including wording and codes the table does not list. POST /api/v1/compliance/10dlc/decode-rejection Pass the raw code, plus the free-text rejectionReason when you have it — the free text disambiguates codes that map to more than one cause:
Response (200 OK):
Read four fields off the card:
  • fix — the single highest-probability corrective step, in the same register the wizard’s rejection banner uses.
  • resubmit_allowed — the verdict that decides your next call. true: amend the payload and resubmit the same brand or campaign. false: the record is locked; start a fresh registration.
  • category — a stable bucket (eligibility, content, identity, throughput, format, dca, unknown) for grouping in your own UI.
  • field — the wizard step the fix applies to (campaign.sample_message, brand.ein, …), so you can deep-link the operator back to the right form.
The decoder is a pure rule engine — it stores nothing, creates nothing, and normalizes the code for you: TCR-30883, Twilio Error 30883, and ein_mismatch all resolve to their canonical entries. An unrecognized code returns an unknown-category card whose fix routes you to Devotel compliance, so a UI that renders the card never shows an empty panel.
Run the decoder before you run remediation. When the card’s fix rewrites a sample message or description, run the corrected payload through the preflight linter before resubmitting — it catches the second-order rejection the fix often introduces.

Worked example: ‘use case mismatch’

A campaign status poll returns:
The reason string names the cause but not a TCR code. The decoder works with one or the other, so pass what you have — the free text alone:
Response — the 30898 card:
resubmit_allowed: true says amend-and-resubmit. Now resubmit the corrected campaign against the same brand — the same call as the initial filing, re-filed like-for-like:
Expect the same 1–5 business days as the initial filing; poll the status endpoint or watch the dashboard notification.

A second worked example: ‘SHAFT sample’

This is the same loop with a content-class card. A reason like "Sample 2 flagged: SHAFT sample" decodes to 30883 — content violation — with resubmit_allowed: true and field: "campaign.sample_message". Rewrite the flagged sample to state the declared vertical and drop the prohibited wording, then re-file the campaign like-for-like. The point of the loop: one decoder call, one corrected submission, and none of it depends on which wording TCR happened to return.

Re-vet vs. re-submit

Rejected content fixes mean re-submission. A low vetting score is a different blocker — no amount of correction to a campaign payload repairs it, and it caps every campaign on the brand, not just the one under review. The score decides the limit, so the remedy is a re-vet, not a resubmission. POST /api/v1/compliance/10dlc/brands/:id/revet
Response (200 OK):
provider names which registrar carries the brand; status is the refreshed brand status once the re-vet kicks off. Before you re-vet, complete the brand record — EIN, legal name matched to IRS records, website, support email on your own domain — the re-vet re-scores the same information, and an unchanged record returns an unchanged score. The registrar rate-limits re-vets (once immediately after registration, then once per roughly three months), so a rate-limited call returns 422 with the registrar’s wait-until text — read it, do not retry in a loop.

Throughput planning: what the score grants

Two endpoints turn the vetting score into the limits you operate under. GET /api/v1/compliance/10dlc/brands/:id/vetting returns the raw result:
vettingScore: null means the EVP is still processing — render “Pending”, not 0. GET /api/v1/compliance/10dlc/brands/:id/throughput derives the carrier-assigned limits from that score:
Response (200 OK):
  • att.class — AT&T assigns a per-campaign throughput class, D→C→B→A (15 → 75 → 240 → 600 messages per minute), at trust-score floors 0/25/50/75.
  • tmobile.tier — T-Mobile assigns a per-brand daily-cap tier (2k / 10k / 40k / 200k / unlimited) that covers every campaign and number on the brand.
  • entity_type (optional query param) — sole-proprietor brands are capped at the 2k tier and Class D regardless of score; pass the registered entity type so the derivation sees the special case.
  • sent_today (optional query param) — message parts already sent in the current rolling 24h window. When you pass it, consumption reports headroom before the silent-filtering zone: ok below 80% of the cap, warning at 80%, critical at 95%, exceeded at 100%.
Both query params are optional; consumption is null until you supply sent_today.

Re-vet, or accept the tier?

Use the warning / critical thresholds as the signal. When your foreground volume consistently lands in the warning band, plan a re-vet — it is rate-limited, so burn it only on a real trajectory change. When the ceiling is the daily cap rather than the class, the constraint is per-brand: assigning more numbers to the campaign does not raise it, and a SOLE_PROPRIETOR upgrade to a full brand record does — that is an entity-type change, not a re-vet.

Re-submission, re-vet: what each costs

TCR charges a new vetting fee per re-submission and per re-vet (typically a few USD), and carriers treat every re-submission as a fresh review — independent timing from your previous attempts.

See also