> ## 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.

# AI Brand-Safety Scan

> Opt-in LLM pass that augments the keyword policy scanner with brand-impersonation, phishing, regulated-claim, hate-speech, and dark-pattern detection — its posture, cost, and lifecycle across compose, campaign launch, and send time.

# AI Brand-Safety Scan

The [policy scanner](/compliance/policy-scanner) runs on every outbound
message and on every keystroke in the compose dialog. It is deterministic —
keyword lists, heuristics, and checksums — so it catches exactly the classes
you can express as a rule: TCPA quiet hours, SHAFT keywords, missing opt-out
text, spam keywords, DLP patterns.

The AI brand-safety scan is an **optional LLM pass layered on top of that
keyword baseline**. It catches the classes a rule can't express, where the
violation lives in the meaning of the copy rather than in any single word. A
phishing message that harvests credentials can contain zero flagged keywords;
an LLM reads it the way a carrier review team would.

All endpoints below are rooted at `https://api.orbit.devotel.io`.

<Note>
  The AI brand-safety scan is a **tenant-owned control** that you invoke per
  request. Nothing runs it on your traffic unless you opt in (`ai: true` on
  the lint endpoint) or until your organization reaches a campaign launch.
  Orbit never uses it to gate your traffic globally. This page is not legal
  advice — confirm your regulatory obligations with counsel.
</Note>

***

## Why an LLM pass complements the keyword scanner

Keyword scanning is the always-on baseline because it is synchronous and free
to run on every send and every keystroke. But the violations that produce
carrier filtering and the largest regulatory settlements are semantic:

* "Your Wells Fargo account is locked — verify at this link" contains no
  keyword on any block list, yet it impersonates a bank.
* "Guaranteed 15% return, withdraw anytime" trips no spam keyword, yet it is
  regulated-finances language a carrier will filter.
* Coercive urgency ("Final notice: pay within 30 minutes to avoid legal
  action") pattern-matches as ordinary promotional copy, yet it is a dark
  pattern that A2P reviewers reject.

Where a keyword pass returns `pass`, the AI pass can still return `warn` or
`block`. The two runs together produce a single merged violations list —
same shape, same verdict roll-up, same enforcement mode (see
[How the verdict is enforced](#how-the-verdict-is-enforced)).

## Posture: opt-in, warn by default

The scan has two enforcement surfaces, and both start from the same default
your [policy scan mode](/compliance/policy-scanner#verdicts-and-the-enforcement-mode)
already has: **warn** — findings are recorded and surfaced, nothing is held.

| Surface                                   | When it runs                              | How you opt in                                                                                                                                                                                                         |
| ----------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Compose-time lint (`POST /messages/lint`) | Only when you ask                         | Pass `ai: true` per request. There is no org-wide toggle, so the dashboard can gate the extra latency behind an "AI scan" affordance instead of paying it on every debounce.                                           |
| Campaign launch gate                      | On every campaign launch, unconditionally | Always on at launch — this is the one place the platform runs the pass without being asked, because a launch commits a multi-thousand-recipient blast and the scan runs exactly once over the body, not per recipient. |

`brand_name` and `industry` are optional hints you pass on the lint request.
They ground the verdict: a pharmacy mentioning prescriptions is legitimate, a
generic retailer mentioning prescriptions is suspicious; a message is checked
for whether it plausibly comes from *your* brand or impersonates a third
party.

When your organization's policy mode is `strict`, every violation the AI pass
returns is **upgraded from `warn` to `block`** — the same severity promotion
the keyword scanner applies in strict mode. A `block` from either scanner
then holds the send with `POLICY_VIOLATION` and a launch with a rejected
campaign. In `warn` mode the same findings are recorded and returned in the
response headers but the send proceeds. In `off` mode the keyword scan is
skipped entirely; the AI pass at lint still runs when you explicitly pass
`ai: true` (you asked for it), and violations stay advisory.

<Note>
  The campaign launch gate is a content gate only. It never reroutes or holds
  carrier termination itself — it rejects or annotates the launch, and the
  normal send path applies the same verdicts per recipient afterward.
</Note>

## The risk classes it catches

Each violation carries one of seven categories:

| Category            | What it catches                                                                                                              | Typical severity |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `impersonation`     | Copy masquerading as a bank, courier, government agency, or healthcare provider — or as your own brand without authorization | `block`          |
| `phishing`          | Credential, OTP, payment-link, or account-verification harvesting                                                            | `block`          |
| `regulated_pharma`  | Drug claims, "buy without prescription" copy                                                                                 | `warn`–`block`   |
| `regulated_finance` | Guaranteed-return language, payday-loan claims, unlicensed investment pitches                                                | `warn`–`block`   |
| `regulated_alcohol` | Alcohol promotion with no age-gating context                                                                                 | `warn`           |
| `hate_or_violence`  | Slurs, incitement, threats — the class the keyword SHAFT list deliberately under-covers                                      | `block`          |
| `dark_pattern`      | Coercive urgency, fake scarcity, manipulative pressure copy                                                                  | `warn`           |

The model is instructed to flag only clear violations — false positives erode
the trust that makes an advisory finding worth acting on. Every violation
comes back with `confidence` on a 0–1 scale so you can sort marginal flags
from certain ones.

## Cost, latency, and why it fails open

One scan is a single LLM call on a small class model:

* **Cost** — on the order of **\$0.0001 per scan** (\~200 input tokens, \~150
  output tokens, 512 max tokens out). At the campaign launch gate that is
  once per launch; at `/messages/lint`, only on requests where you passed
  `ai: true`.
* **Latency** — P95 under **1.5 seconds**; bounded by a 6-second hard abort,
  so a slow model response can never hold a compose request or a launch
  handler past the ingress window.
* **Rate limiting** — the lint endpoint is generously rate-limited
  (120 requests per minute per organization) to support as-you-type debounce;
  the AI pass consumes the same budget, so invoking it on every keystroke is
  the misuse it was designed to survive but not encourage.

**Fail-open is deliberate.** On any scanner error — model timeout, abort,
schema mismatch, upstream outage — the scan returns `pass` with
`failedOpen: true` and the send or launch proceeds on the keyword baseline
alone. The rationale: the keyword scanner already covers the deterministic
compliance classes, so an AI outage degrades you to the pre-LLM behaviour
rather than to a traffic halt. A brand-safety outage must never block
legitimate sends, and this module never gets the right to. When a scan fails
open, the response says so — treat `failed_open: true` as "advisory layer
unavailable," not as "content was clean."

## Where the scan runs in the send lifecycle

The same scanner appears at three points, earliest first:

1. **Compose-time lint** — `POST /messages/lint` with `ai: true`. Read-only;
   nothing is sent or recorded against a message. The dashboard compose dialog
   surfaces the verdict inline before you commit. Lint with `ai: true` during
   drafting, not on every debounce.
2. **Campaign launch gate** — runs automatically when a campaign launches,
   once over the body-only content (recipient-dependent rules like quiet hours
   still run per recipient afterward). In strict mode a `block` verdict
   **rejects the launch**; in warn mode the findings are attached to the
   launch response headers.
3. **Send-time lint** — the per-message policy scan on the actual send path
   stays keyword-only by default; the AI-verdict surface you saw at lint and
   launch uses the same violations shape, so what you previewed is what the
   send path would have enforced in strict mode.

## Audit and evidence

Where a scan runs, what it decided is recorded in the customer's own
vocabulary rather than hidden in a model response:

* **Lint response** — the `ai_brand_safety` block carries the verdict, the
  full violations list with confidence, `failed_open`, and the model
  identifier.
* **Launch response** — AI findings are merged with keyword findings into one
  violations list, returned in the `X-Policy-Violations` response header and,
  on a strict-mode block, in the error details.
* **Evidence binder** — compliance scan posture (your policy mode, and the
  gates that run along your send path) is part of what the
  [evidence binder](/compliance/evidence-binder) compiles into the audit
  pack you hand to an auditor. Generate it when you need the posture on
  paper, not per send.

## Worked example

Lint a draft with the AI pass and your grounding hints:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/lint \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "body": "WELLS FARGO ALERT: Your account is suspended. Verify now: https://wf-verify.example.com — failure = legal action",
    "ai": true,
    "brand_name": "Acme Nutritional Supplements",
    "industry": "retail"
  }'
```

```json 200 theme={null}
{
  "mode": "warn",
  "verdict": "pass",
  "violations": [],
  "spam_score": 12,
  "ai_brand_safety": {
    "verdict": "block",
    "violations": [
      {
        "rule": "ai_brand_safety_impersonation",
        "category": "impersonation",
        "severity": "block",
        "message": "The message claims to be Wells Fargo, a bank the operator (a retail supplements brand) is not affiliated with, and pressures the recipient to verify an account.",
        "suggestion": "Remove the bank's name and the account-suspension framing; identify your own brand and link to your own verified domain.",
        "confidence": 0.97
      },
      {
        "rule": "ai_brand_safety_phishing",
        "category": "phishing",
        "severity": "block",
        "message": "Directs the recipient to a third-party lookalike verification URL to harvest account credentials.",
        "suggestion": "Send account-related links only from your registered sender identity and owned domain.",
        "confidence": 0.94
      }
    ],
    "failed_open": false,
    "model": "claude-haiku"
  },
  "blocked": false
}
```

In `warn` mode the keyword verdict was `pass`, the AI verdict was `block`,
and `blocked` is `false` — the findings are advisory. Had the organization
been in `strict` mode, both AI violations would have been promoted to
`block` severity, `blocked` would be `true`, and the same content on a
campaign launch would have been rejected with `POLICY_VIOLATION` until the
copy was remediated along the suggestions above.

## Related pages

* [Pre-Send Policy Scanner & DLP](/compliance/policy-scanner) — the keyword
  baseline, enforcement modes, and the compose linter the AI pass extends
* [Send gates](/compliance/send-gates) — BAA, quiet hours, DNC, and RND gates
  that run alongside the scanner at send time
* [Compliance evidence binder](/compliance/evidence-binder) — export your
  compliance posture as an audit-ready pack
