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

# Pre-Send Policy Scanner & DLP

> Configure the pre-send policy scanner (TCPA, SHAFT, GDPR, DLP, spam keywords) and the organization-wide scan mode that decides whether a flagged message is blocked, warned, or passed.

# Pre-Send Policy Scanner & DLP

Every outbound message (SMS, MMS, WhatsApp, email, RCS, and IM channels) runs
through a policy scanner **before it dispatches to the carrier**. The scanner
returns a verdict of `pass`, `warn`, or `block`, and your organization's scan
mode decides what happens next: block the send, let it through and record the
finding, or skip the scan entirely.

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

<Note>
  The policy scanner is a **tenant-owned control**. You set the mode for your
  own organization; it defaults to `warn` (scan everything, block nothing).
  Orbit never uses it to gate your traffic globally. This page is not legal
  advice — confirm your TCPA / GDPR / PCI obligations with counsel.
</Note>

***

## What the scanner checks

Each rule applies to specific channels, so a channel with no applicable rules
returns a clean `pass` with zero violations.

| Rule                               | Channels                                                                                    | What it checks                                                                                                                                                                                                              |
| ---------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TCPA quiet hours                   | SMS, WhatsApp (US `+1` recipients)                                                          | Marketing sends outside 08:00–21:00 recipient-local time. The recipient's timezone is resolved from the NANP area code, or you can pass an explicit timezone/scheduled hint. Transactional traffic (OTP, alerts) is exempt. |
| SHAFT content                      | US SMS                                                                                      | Sex, hate, alcohol, firearms, tobacco, and cannabis keywords. US carriers filter or fine this traffic, and a hate-speech hit is never eligible for any A2P route.                                                           |
| Missing opt-out                    | Marketing channels (SMS by default; configurable to include WhatsApp, RCS, Viber, Telegram) | A message that looks promotional but carries no opt-out phrase (STOP, PARAR, ARRÊTER, 退订, across 14 locales).                                                                                                               |
| Short URLs                         | All channels                                                                                | Public shorteners (`bit.ly`, `t.co`, …) that carriers aggressively filter. Use the platform's [branded short links](/api-reference/links) instead.                                                                          |
| Spam-keyword score                 | SMS, WhatsApp, email, RCS                                                                   | A SpamAssassin-style score from 0–100. Scores ≥ 80 block; 60–79 warn. The response lists every matched rule so you can rephrase the call-to-action.                                                                         |
| GDPR sender identity               | Email to EU recipients (by recipient country or EU TLD)                                     | Requires a From display name and a physical postal address in the footer (GDPR art. 13 / ePrivacy).                                                                                                                         |
| Country registration & sender gate | All channels                                                                                | When a destination country requires sender registration (India DLT, KSA, Turkey) or restricts permitted sender types, an unregistered or mismatched sender blocks. Uncatalogued countries are never blocked.                |
| DLP — sensitive data               | All channels                                                                                | A full credit card / PAN, US Social Security number, or IBAN in the message body. Passport detection is opt-in (see [DLP specifics](#dlp-sensitive-data-scanning)).                                                         |

## Verdicts and the enforcement mode

The scanner collapses all matched rules into one verdict:

* `pass` — no violations. The send proceeds.
* `warn` — at least one advisory rule fired. The send proceeds and the
  violations are recorded on the message metadata and returned in the
  `X-Policy-Violations` response header.
* `block` — at least one blocking rule fired (SHAFT content, a spam score
  ≥ 80, DLP, or an unregistered-sender country gate), or TCPA quiet-hours
  promoted to enforceable while the org is in `strict` mode on the SMS
  marketing lane.

Your organization's **policy scan mode** decides what a verdict does:

| Mode             | Effect                                                                                                                           |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `warn` (default) | The send always proceeds; `block`-verdict findings are recorded and surfaced in `X-Policy-Violations`.                           |
| `strict`         | A `block` verdict rejects the send with `POLICY_VIOLATION`. TCPA quiet-hours on the SMS marketing lane also becomes enforceable. |
| `off`            | The scan is skipped entirely on the send path.                                                                                   |

New organizations read back `warn` until an owner sets a mode explicitly.

## Set the scan mode

Read the current mode (any workspace member):

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/settings/compliance/policy-scan-mode \
  -H "Authorization: Bearer $API_KEY"
```

```json 200 theme={null}
{
  "data": { "policy_scan_mode": "warn" },
  "meta": { "request_id": "req_9f2c…", "timestamp": "2026-08-31T12:00:00Z" }
}
```

Set the mode (owner role required; applies to the very next send and is
written to the audit log):

```bash theme={null}
curl -X PATCH https://api.orbit.devotel.io/api/v1/settings/compliance/policy-scan-mode \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"policy_scan_mode": "strict"}'
```

<Warning>
  A `PATCH` with anything other than `strict`, `warn`, or `off` returns
  `422 VALIDATION_ERROR`.
</Warning>

## Error taxonomy on the send path

Three distinct failure surfaces come out of the scanner, so you can tell a
real content block from a scanner outage:

| Code                             | HTTP status                                                                                                | Meaning                                                                                                                                                                                                   |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POLICY_VIOLATION`               | 400 (content-class block: SHAFT, spam score, DLP, country gate) or 422 (TCPA quiet-hours in `strict` mode) | The scanner returned a `block` verdict while your org is in `strict` mode. The response body carries the violations list — fix the content and retry.                                                     |
| `POLICY_SCAN_MODE_LOOKUP_FAILED` | 503                                                                                                        | The send-path guard could not read your organization's mode from the database and its short-lived cache was cold. The send fails closed. Retry; this is a transient lookup failure, not a policy verdict. |
| `POLICY_SCANNER_UNAVAILABLE`     | 503                                                                                                        | The scanner itself threw (bad import, malformed rule). Also fails closed so an unscanned message never ships. Retry.                                                                                      |

A violation response looks like this:

```json 400/422 theme={null}
{
  "error": {
    "code": "POLICY_VIOLATION",
    "message": "Outbound message blocked by policy scanner — see details.",
    "status": 400,
    "details": {
      "violations": [
        {
          "rule": "shaft_alcohol",
          "severity": "block",
          "message": "SMS content contains SHAFT-restricted keyword (alcohol): \"vodka\". US carriers will filter or fine this traffic.",
          "suggestion": "Remove the flagged term, or route through an age-gated campaign with carrier approval."
        }
      ]
    },
    "request_id": "req_…"
  },
  "meta": { "timestamp": "…" }
}
```

Each violation names the rule, its severity (`enforce`, `block`, or `warn`),
an operator-facing message, and a suggestion. Hate-speech violations
deliberately never echo the matched slur back.

## DLP — sensitive-data scanning

The DLP rule runs on every channel because a card number is equally sensitive
on SMS, WhatsApp, email, or RCS. It detects:

* **Credit card / PAN** — validated with the Luhn checksum **and** a
  card-network prefix/length check (Visa, Mastercard, Amex, Diners, Discover,
  JCB, UnionPay). A random grouped number — a phone list, an order id — is
  rejected on the checksum, so false positives are rare.
* **US Social Security number** — requires an explicit dash or space
  separator and passes the SSA's area/group/serial validity rules.
* **IBAN** — validated with the ISO 7064 mod-97 checksum **and** the
  per-country registry length. Space-grouped IBANs are handled.
* **Passport** — opt-in only. A bare 6–9 character token is too ambiguous to
  block by default, so detection additionally requires a `passport`-context
  keyword next to the token. Ask support if you want this enabled for your
  organization.

Privacy is built into the result shape: findings carry **character offsets
and a category only — never the matched text**. The detector cannot leak a
card or SSN into logs, response headers, or the audit trail. When a tenant
opts into redact mode, the original body is substituted with typed sentinels
(`[REDACTED_CARD]`, `[REDACTED_SSN]`, `[REDACTED_IBAN]`, `[REDACTED_PASSPORT]`)
before dispatch.

DLP is on by default at severity `block` on the universal PCI/PII floor
(cards, SSN, IBAN). Contact support to tune it (flag-only `warn`, auto-redact
`redact`, narrow the categories, or opt into passport detection).

## Compose-time live linter

You don't have to wait for a send to find a violation. `POST /messages/lint`
runs the **same scanner** against a draft body and returns the same verdict,
violations, spam score, and matched spam rules — without sending anything.
The dashboard compose dialog calls it debounced as you type so inline warnings
appear before you send.

```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": "BIG SALE this weekend — wine tasting vodkas!",
    "to": "+14155552671"
  }'
```

```json 200 theme={null}
{
  "verdict": "block",
  "violations": [
    {
      "rule": "shaft_alcohol",
      "severity": "block",
      "message": "SMS content contains SHAFT-restricted keyword (alcohol): \"vodka\".",
      "suggestion": "Remove the flagged term, or route through an age-gated campaign."
    }
  ],
  "spam_score": 34
}
```

Lint accepts `sms`, `whatsapp`, `email`, and `rcs` channels plus optional
`to`, `subject`, `from_name`, `recipient_country`, and `scheduled_at` hints,
so the linter sees the same context the send-path scan would. You can also
pass `ai: true` (and optional `brand_name` / `industry`) to augment the
keyword scan with an AI brand-safety pass for impersonation and phishing
patterns; it fails open on an AI outage.

<Note>
  `/messages/lint` is read-only and intentionally rate-limited generously
  (120 requests per minute per organization) to support live-as-you-type
  debounce patterns.
</Note>

## Tenancy posture

The policy scanner is **your** control, on your traffic. An owner in your
organization picks the mode, and the read endpoint is open to any member so
your compliance panel can display it. Orbit doesn't gate outbound on the
scanner globally, and no platform-wide default is forced on you beyond the
`warn` starting point. The changes you make are written to your own audit log
(`settings.policy_scan_mode_updated`).

## Related pages

* [Settings API reference — compliance](/api-reference/settings) — the full
  `/settings/compliance/*` surface
* [Error codes](/reference/error-codes) — `POLICY_VIOLATION`,
  `POLICY_SCAN_MODE_LOOKUP_FAILED`
* [Send gates](/compliance/send-gates) — BAA, quiet hours, DNC, RND gates that
  run alongside the scanner
* [Opt-outs & suppression](/compliance/opt-out-suppression) — how opt-out
  keywords set suppression
