> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 10DLC preflight: score, verdict, and finding codes

> Reference for the 10DLC TCR pre-submit linter — the engine, the score/verdict/findings contract, every rule it checks, and how to wire it into a pre-submit CI gate.

# 10DLC preflight: score, verdict, and finding codes

A 10DLC brand + campaign submission goes through TCR vetting, and most
first-time rejections come from a small set of deterministic content and
completeness patterns — missing opt-out wording, public link shorteners,
placeholder sample messages, an opt-in description that is too thin.
Every rejected resubmission costs a new vetting fee and restarts the
review clock. The preflight engine scores your submission against that
known rejection-pattern catalog **before** you pay the fee, and returns
a score, a verdict, and per-field findings with the exact offending
fragment and a concrete rewrite suggestion.

This page is the reference for that engine: the three endpoint surfaces
that run it, the request/response contract, every rule code and
remediation, rate limits, and a worked CI gate that blocks a submission
until the verdict is `pass`. For the end-to-end registration walkthrough
(registering the brand, creating the campaign, tracking vetting), see
[Register for 10DLC](/guides/10dlc-registration).

<Warning>
  This page describes a tenant-owned compliance control. It is **not
  legal advice.** Whether a submission satisfies TCR and carrier
  requirements is your brand's responsibility — confirm with qualified
  counsel.
</Warning>

***

## Three surfaces, one engine family

Three endpoints run preflight engines. The two 10DLC surfaces both run
the same engine (`tcr-preflight/v1`); the short-code surface runs the
parallel engine (`shortcode-preflight/v1`) against a program-brief
payload shape. All routes below are rooted at
`https://api.orbit.devotel.io/api/v1`:

| Endpoint | Body | What it lints |
| - | - | - |
| `POST /compliance/10dlc/preflight` | `brand` + `campaign` + optional `brand_vetting_score` | An ad-hoc 10DLC payload, before any registration exists |
| `POST /compliance/10dlc/wizard/preflight` | optional `expected_msg_per_day_per_number`, `brand_vetting_score` | The 10DLC wizard draft persisted on your organization |
| `POST /numbers/short-codes/preflight` | `programBrief` + optional `businessName` | A short-code program brief (see [Short-code preflight](/compliance/shortcode-preflight)) |

Pick the surface by **what you want linted**, not by the rule set — the
two 10DLC surfaces emit identical finding structures. The ad-hoc
endpoint needs no persisted state; the wizard endpoint reads the draft
saved through the wizard flow. The short-code engine is a sibling
surface: same `score`/`verdict`/`findings[]` contract, different rule
catalog and payload shape. All three endpoints are idempotent reads —
nothing is sent to TCR or a carrier, and nothing is created or stored.

***

## Request and response contract

### Ad-hoc 10DLC payload

`POST /compliance/10dlc/preflight` accepts brand and campaign objects
shaped like the submission you intend to send:

```json theme={null}
{
  "brand": {
    "display_name": "Acme Corp",
    "entity_type": "PRIVATE_PROFIT",
    "ein": "12-3456789",
    "email": "compliance@acme.com",
    "website": "https://acme.com"
  },
  "campaign": {
    "usecase": "MARKETING",
    "description": "Acme sends weekend promotional offers to customers who opt in on our checkout page.",
    "sample_message": ["Acme: 20% off today. Reply STOP to opt out."],
    "message_flow": "Customers opt in at checkout. They can reply STOP at any time to opt out.",
    "help_message": "Reply HELP for assistance or email support@acme.com.",
    "optout_message": "You are unsubscribed. Reply START to re-subscribe.",
    "expected_msg_per_day_per_number": 5000
  },
  "brand_vetting_score": 45
}
```

* `brand_vetting_score` — your current TCR vetting score (0–100) when
  you already hold one. It drives the throughput-tier rule
  (`R-CAMP-THROUGHPUT-TIER`); omit it or pass `null` for an un-vetted
  brand and the linter assumes the Low Volume Standard baseline tier.
* `campaign.expected_msg_per_day_per_number` — your declared forecast
  volume per number per day, used by the same tier rule.

### Wizard payload

`POST /compliance/10dlc/wizard/preflight` takes no brand or campaign
body — it loads the draft you saved through
`PATCH /compliance/10dlc/wizard/draft` and lints that. Both body fields
are optional:

* `expected_msg_per_day_per_number` — forecast volume from your
  volume-planning widget, applied on top of the persisted draft.
* `brand_vetting_score` — current score, same semantics as the ad-hoc
  endpoint.

Linting the persisted draft instead of a client-side copy removes the
risk that your findings describe an older revision than the one you are
about to submit.

### Response shape

Both 10DLC endpoints return the same envelope:

```json theme={null}
{
  "data": {
    "score": 82,
    "verdict": "warn",
    "findings": [
      {
        "ruleId": "R-CAMP-SAMPLE-CTA-STOP",
        "severity": "warn",
        "field": "campaign.sample_message[0]",
        "message": "Sample message missing opt-out wording.",
        "match": "Acme: 20% off today.",
        "suggestion": "Append \"Reply STOP to opt out.\""
      }
    ],
    "engine": "tcr-preflight/v1"
  }
}
```

* `score` — 0–100; 100 means no known pattern matched. Each `error`
  finding deducts 25 points, each `warn` 8, each `info` 2.
* `verdict` — `block` when any `error`-severity finding is present,
  `warn` when any `warn` finding is present or the score lands below 75,
  otherwise `pass`.
* `findings[]` — one entry per rule hit. `field` is the dot-path of the
  offending field (e.g. `campaign.sample_message[2]`); `match` quotes
  the exact fragment when one exists; `suggestion` is the rewrite that
  resolves the finding.
* `engine` — the rule-engine version, stamped on every response so you
  can correlate score changes when the catalog evolves.

**Lint pass is not carrier approval.** A `pass` verdict means "no known
rejection pattern matched." Your submission still goes through the
normal TCR and per-carrier review, which includes human vetting. Treat
the preflight as the deterministic first pass that removes the
rejectable-by-pattern defects; the carrier review that follows is the
authoritative pass.

***

## Failure catalogue

Each finding carries a stable `ruleId`. The table covers every code the
`tcr-preflight/v1` engine emits, with the remediation for each.

| `ruleId` | Severity | Trigger | Remediation |
| - | - | - | - |
| `R-BRAND-EIN` | error | For-profit entity type without a 9-digit EIN | Provide the EIN in NN-NNNNNNN format, or change `entity_type` to `SOLE_PROPRIETOR` |
| `R-BRAND-EMAIL-DOMAIN` | warn | Brand email on a free-mail domain (gmail.com, outlook.com, …) | Use an email at the brand's own domain |
| `R-BRAND-WEBSITE` | warn | No website URL | Provide the primary website; it should match the email domain |
| `R-CAMP-DESC-LEN` | error | Description under 40 characters | Describe who opts in, what sends, and the business value — aim for 150–300 characters |
| `R-CAMP-DESC-GENERIC` | warn | Description echoes a generic template ("We send messages to our customers") | Name the exact opt-in moment and the message type |
| `R-CAMP-SAMPLES-COUNT` | warn | Fewer than 2 sample messages | Provide 2–5 distinct samples covering the real scenarios (welcome, reminder, confirmation) |
| `R-CAMP-SAMPLE-CTA-STOP` | warn | No sample mentions the STOP opt-out | Append "Reply STOP to opt out." to at least one sample |
| `R-CAMP-SAMPLE-PLACEHOLDER` | error | Sample is a bare template placeholder (`Hi {{firstName}}`) | Render the placeholder with a realistic example |
| `R-CAMP-SAMPLE-CLICK-HERE` | warn | Bare "click here" in a sample | Name the destination ("view your order", "open the form") |
| `R-CAMP-SAMPLE-SHORTENER` | error | Public URL shortener (bit.ly, t.co, tinyurl.com, goo.gl, ow\.ly, buff.ly, is.gd, rb.gy, shorturl.at, cutt.ly) in a sample | Replace it with a link on your own branded domain |
| `R-CAMP-SAMPLE-SHAFT` | error | SHAFT-C content (sex, hate, alcohol, firearms, tobacco, cannabis) in a sample | Remove the content; SHAFT verticals require a dedicated age-gated campaign with explicit carrier approval |
| `R-CAMP-SAMPLE-BLOCKLIST` | error | A carrier content-blocklist phrase (MEF / UCC §3.4 filters) in a sample | Remove or reword the phrase; regulated verticals need explicit carrier approval |
| `R-CAMP-HELP` | warn | `help_message` omits the HELP/INFO keyword pattern | Use the canonical pattern: "For HELP, reply HELP. Contact: [support@example.com](mailto:support@example.com)". |
| `R-CAMP-OPTOUT` | error | `optout_message` omits STOP/UNSUBSCRIBE/QUIT/END/CANCEL | Use the canonical text: "You have been unsubscribed and will receive no further messages." |
| `R-CAMP-FLOW` | error | `message_flow` under 40 characters | Describe the opt-in moment concretely (page, form, checkbox) |
| `R-CAMP-AI-USECASE` | warn | Message flow or use case signals AI-driven content but the description does not disclose it | State what the AI does, that a human is reachable, and how to opt out |
| `R-CAMP-THROUGHPUT-TIER` | warn | Declared `expected_msg_per_day_per_number` exceeds the cap on your eligible vetting tier | Lower the declared volume, or raise your vetting score to qualify for a higher daily-cap tier |
| `R-CAMP-THROUGHPUT-TIER` | error | Declared volume exceeds the tier cap by more than 5× | Same remediation — the mismatch is severe enough to reject outright |

The scoring weights and verdict thresholds above (`error` −25, `warn`
−8, `info` −2; `warn` verdict below 75) are fixed for
`tcr-preflight/v1`. If the catalog or weights change, the `engine` label
rolls to a new version stamp, which lets you correlate a score change to
an engine release rather than to your own payload edit.

***

## Rate limits and correlation

* Both 10DLC preflight endpoints are rate-limited to **30 requests per
  minute** per organization. The wizard surface additionally enforces an
  admin role.
* A `429` means you exceeded the per-minute budget; back off and retry
  after the window closes. There is no monthly quota.
* Every response carries `meta.request_id`. Log that ID in the job that
  calls the linter — the linter is deterministic for a given payload and
  catalog version, so pairing the request ID with the result lets you
  reconstruct any deviation between two runs of the same payload.

***

## Worked example: pre-submit CI gate

Run the preflight as a gate in the pipeline that prepares a 10DLC
submission, and fail the job on any verdict other than `pass`. The
linter is a pure function of the payload and engine version, so a green
run means there is no known rejection pattern left to match at this
engine version.

```bash theme={null}
#!/usr/bin/env bash
# Preflight the 10DLC payload before the submit step runs.
set -euo pipefail

PAYLOAD_FILE="${1:?usage: preflight.sh <payload.json>}"

RESP="$(curl -sf -X POST \
  https://api.orbit.devotel.io/api/v1/compliance/10dlc/preflight \
  -H "X-API-Key: ${ORBIT_API_KEY:?set ORBIT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d @"${PAYLOAD_FILE}")"

VERDICT="$(printf '%s' "${RESP}" | jq -r '.data.verdict')"

if [ "${VERDICT}" != "pass" ]; then
  echo "10DLC preflight verdict=${VERDICT} — resolve findings before submit:" >&2
  printf '%s' "${RESP}" | jq -r '
    .data.findings[]
    | "  [" + .severity + "] " + .ruleId + " (" + .field + "): "
      + .message
      + (if .suggestion then "\n      Fix: " + .suggestion else "" end)
  ' >&2
  exit 1
fi

echo "10DLC preflight pass (score $(printf '%s' "${RESP}" | jq '.data.score'))"
```

Treat `warn` as a gate failure. A `warn` verdict means a finding
carriers frequently reject on is present (missing STOP wording, too few
samples, throughput-tier mismatch often not fatal but worth fixing).
Failing only on `block` lets those through. Log verdict + score on
every run; when a submission is rejected despite a `pass`, diff the
stored payload against what TCR saw — the `engine` stamp tells you
whether the rule catalog moved between the two runs.

***

See also:

* [Register for 10DLC](/guides/10dlc-registration) — the end-to-end
  walkthrough: register the brand, create the campaign, track vetting.
* [10DLC rejections and re-vet](/guides/10dlc-rejections-and-revet) —
  decode a rejection code and correct the filing.
* [Short-code preflight](/compliance/shortcode-preflight) — the sibling
  engine and rule catalog for short-code program briefs.
* [10DLC brand and campaign profiles](/compliance/10dlc-brand-campaign-profiles)
  — how the brand and campaign objects the linter scores map to the TCR
  registration lifecycle.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.