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

# API recipes: the US 10DLC pre-send chain

> Runnable curls for the four gates every US SMS marketing send passes — read a sender's 10DLC registration state before you allow the send, file the brand and campaign registration over the API, pre-check quiet-hours and consent for the destination, and tag the send body with the marketing versus transactional lane.

# API recipes: the US 10DLC pre-send chain

The [baseline compliance posture for US SMS traffic](/guides/10dlc-marketing-baseline) describes the gates a US SMS send passes — 10DLC registration state, opt-out suppression, quiet-hours, and the marketing/transactional lane split — but documents them conceptually with no runnable loop. These recipes assemble that chain over the API: the exact calls, the expected envelopes, and the branch you take on each outcome. Base URL is `https://api.orbit.devotel.io/api/v1` throughout; brand and campaign **writes** require an owner/admin key, every read takes any authenticated key.

<Note>
  These are **tenant-owned controls**. Orbit supplies and enforces the gates; your organization files the registrations, seeds the suppression lists, and makes the go-live call. These recipes are not legal advice.
</Note>

## 1. Pre-flight: read the sender's registration state before you allow a send

A marketing send on an unregistered (or rejected) brand is not a send at all — carriers filter it downstream. `GET /compliance/10dlc/status` is the one read that answers "is this sender allowed to file traffic":

```bash cURL theme={null}
curl https://api.orbit.devotel.io/api/v1/compliance/10dlc/status \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

`200` returns the combined state machine:

```json 200 theme={null}
{
  "data": {
    "overall_status": "ready",
    "provider": "telnyx",
    "brands": [
      { "brandId": "brnd_01HXV…", "status": "APPROVED", "rejectionReason": null }
    ],
    "campaigns": [
      { "campaignId": "cmp_01HXV…", "brandId": "brnd_01HXV…", "usecase": "MARKETING", "status": "ACTIVE" }
    ]
  },
  "meta": { "request_id": "req_10dlc_status", "timestamp": "2026-09-29T12:00:00.000Z" }
}
```

Treat `data.overall_status` as the send gate, and branch exactly once per send-batch:

| `overall_status` | Meaning | Action |
| - | - | - |
| `ready` | At least one brand + campaign is approved | Send — then run recipes 3 and 4 on each destination |
| `pending` | A filing is mid-vetting | Hold the queue; TCR vetting takes days, never busy-poll tighter than hourly |
| `not_registered` | No brand or campaign on file | Block the send; run recipe 2 first |
| `not_configured` | The 10DLC provider has no credentials | Block the send; an operator finishes provider setup — a retry loop cannot fix this |

Two finer branches matter before you block on `pending`: read the per-item `status` on each brand and campaign (a rejected campaign returns its `rejectionReason`, which you decode with `POST /compliance/10dlc/decode-rejection` — see the [rejections and re-vet guide](/guides/10dlc-rejections-and-revet)), and match the `usecase` on the active campaign against the lane you intend to send. A `MARKETING` body on a `CUSTOMER_CARE` campaign is the classic use-case mismatch rejection from the [baseline posture guide](/guides/10dlc-marketing-baseline#the-two-lanes-marketing-vs-transactional).

Canonical page: [10DLC registration](/guides/10dlc-registration).

## 2. Register the brand and campaign over the API

When recipe 1 returns `not_registered`, file the registration with two writes. Run the free pre-flight linter first — it scores the same fields TCR rejects on before any vetting fee moves:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/10dlc/preflight \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "brand": {
      "display_name": "Acme Alerts",
      "company_name": "Acme Widgets, Inc.",
      "ein": "12-3456789",
      "entity_type": "PRIVATE_PROFIT",
      "vertical": "RETAIL"
    },
    "campaign": {
      "usecase": "MARKETING",
      "description": "Promotional offers and back-in-stock alerts for opted-in customers.",
      "sample_message": [
        "Acme: Your weekend 20%-off code is WKD20. Shop at https://acme.example/sale. Reply STOP to opt out."
      ],
      "optout_message": "You are unsubscribed from Acme promotions. No more messages will be sent."
    }
  }'
```

Lint findings name the field, the offending wording, and a suggested rewrite — fix them, re-lint, and only then file the brand:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/10dlc/brand \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "PRIVATE_PROFIT",
    "display_name": "Acme Alerts",
    "company_name": "Acme Widgets, Inc.",
    "ein": "12-3456789",
    "phone": "+14155551000",
    "street": "500 Pine St",
    "city": "Seattle",
    "state": "WA",
    "postal_code": "98101",
    "country": "US",
    "email": "compliance@acme.example",
    "website": "https://acme.example",
    "vertical": "RETAIL"
  }'
```

`201` returns `{ "brandId": "brnd_01HXV…", "status": "PENDING" }`. Sole proprietors (`entity_type: "SOLE_PROPRIETOR"`) omit `ein` and anchor on a verified phone number instead. With a brand id in hand, file the campaign the same way:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/10dlc/campaign \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "brand_id": "brnd_01HXV…",
    "usecase": "MARKETING",
    "description": "Promotional offers and back-in-stock alerts sent to customers who opted in via the checkout page or the JOIN keyword. Opt-in is timestamped and stored per contact.",
    "sample_message": [
      "Acme: Your weekend 20%-off code is WKD20. Shop at https://acme.example/sale. Reply STOP to opt out.",
      "Acme: The Trail Boot is back in stock in your size. Grab it: https://acme.example/boot. Reply STOP to opt out."
    ],
    "message_flow": "Users opt in on the checkout page (unchecked box with disclosure text) or by texting JOIN to our published number; consent is recorded with source and timestamp.",
    "help_message": "Acme Alerts: help at https://acme.example/help or reply STOP to opt out.",
    "optout_message": "You are unsubscribed from Acme promotions. No more messages will be sent."
  }'
```

`201` returns the campaign id with `status: "PENDING"`. The decision table for the filing loop:

| Signal | Branch |
| - | - |
| `422 VALIDATION_ERROR` naming a field | Fix that field — `description`/`message_flow` need 40+ characters, `optout_message` 20+; never retry the same body |
| `201` with `status: "APPROVED"` | Re-poll recipe 1; `overall_status` flips to `ready` |
| `201` with `status: "REJECTED"` and a `rejectionReason` | Feed it to `POST /compliance/10dlc/decode-rejection`, apply the fix card, re-file |
| `403` | The key is not owner/admin — brand and campaign writes are admin-gated |

Both filings write the [audit log](/guides/audit-log) (`compliance.10dlc_brand_registered`, `compliance.10dlc_campaign_registered`).

Canonical pages: [10DLC registration](/guides/10dlc-registration), [10DLC registration wizard](/guides/10dlc-wizard) (the operator flow with save-and-resume).

## 3. Suppression pre-check: quiet-hours and consent before the send

Registration answers "can this brand send"; this pair answers "can this destination receive, right now." Run both reads before dispatching a marketing send — the send path re-checks both and hard-rejects, so a pre-check turns a rejected send into a queued one.

**Quiet-hours preview** — the same evaluator the send gate runs, read-only:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/quiet-hours/preview?phone=%2B14155552671&channel=sms&timezone_override=America/New_York" \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

```json 200 theme={null}
{
  "data": {
    "allowed_now": false,
    "local_hour": 22,
    "local_timezone": "America/New_York",
    "window_start_local": "08:00",
    "window_end_local": "21:00",
    "next_allowed_at": "2026-09-30T12:00:00.000Z",
    "reason": "outside_window",
    "channel": "sms"
  },
  "meta": { "request_id": "req_qh_preview", "timestamp": "2026-09-29T02:00:00.000Z" }
}
```

Branch on `data.allowed_now`: `false` means schedule the send for `data.next_allowed_at` — never retry inside the window. Pass `timezone_override` when your CRM knows the recipient's timezone; otherwise the NANP area code resolves it. Drop `timezone_override` and omit your own inference entirely — the preview mirrors the gate.

**Consent and suppression lookup** — the ledger entry every send gate reads:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/consent/lookup?identifier=%2B14155552671&channel=sms" \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

```json 200 theme={null}
{
  "data": {
    "status": "opted_out",
    "state": "opted_out",
    "marketing_eligible": false,
    "revoked_at": "2026-09-14T18:31:00.000Z",
    "checked_at": "2026-09-29T02:00:00.000Z",
    "identifier_kind": "phone"
  },
  "meta": { "request_id": "req_consent_lookup", "timestamp": "2026-09-29T02:00:00.000Z" }
}
```

The branch table — `marketing_eligible` is false unless a live `opted_in` grant exists, so treat it as the send/no-send bit and use `status` for the why:

| `status` | Marketing decision |
| - | - |
| `opted_in` | Send allowed (still gated by quiet-hours preview) |
| `opted_out` | Never send; the send path would reject anyway |
| `pending` | A double-opt-in prompt is outstanding — wait for confirmation |
| `no_record` / `unknown` | Marketing blocked; consent cannot be positively confirmed |
| `expired` | A time-bounded opt-in lapsed — re-confirm before sending |

When a send does slip past your pre-check — a destination opted out between preview and dispatch — the send endpoint answers with a rejection envelope you branch on by `error.code`:

```json 422 — opted-out recipient theme={null}
{
  "error": {
    "code": "RECIPIENT_OPTED_OUT",
    "message": "Recipient has opted out of this channel",
    "status": 422
  },
  "meta": { "request_id": "req_send_optout", "timestamp": "2026-09-29T02:00:04.000Z" }
}
```

```json 422 — inside quiet hours theme={null}
{
  "error": {
    "code": "QUIET_HOURS_BLOCKED",
    "message": "Send paused — recipient is in quiet hours (America/New_York). Next allowed at 2026-09-30T12:00:00.000Z.",
    "status": 422,
    "details": {
      "channel": "sms",
      "reason": "outside_window",
      "next_allowed_at": "2026-09-30T12:00:00.000Z"
    }
  },
  "meta": { "request_id": "req_send_quiet", "timestamp": "2026-09-29T02:00:04.000Z" }
}
```

Both rejections are per-recipient business outcomes, not provider faults: `RECIPIENT_OPTED_OUT` is terminal for that destination (remove them from the campaign audience), `QUIET_HOURS_BLOCKED` means re-schedule at `error.details.next_allowed_at`, and `QUIET_HOURS_TIMEZONE_UNKNOWN` (the fail-closed sibling when no timezone can be resolved) means set the contact's timezone or review your `unknown_timezone_policy` on the [quiet-hours configuration](/guides/quiet-hours-configuration) page. Seeding a migrated suppression list is a one-time CSV import — `POST /compliance/suppression-list/import` — covered end to end in the [suppression entry-point guide](/guides/suppression-three-entry-points).

Canonical pages: [Opt-Out & Suppression Lists](/compliance/opt-out-suppression), [Quiet hours: org-wide channel gates](/guides/quiet-hours-configuration).

## 4. Tag the lane: marketing versus transactional on the send body

Every gate above reads the same lane tag on the send itself. The send body carries it on the `metadata` envelope — `traffic_lane` always wins, `message_type` tags the marketing consent gates, and campaign-origin sends default to `marketing` when neither is set:

```bash cURL — marketing promotion theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "channel": "sms",
    "body": "Acme: Your weekend 20%-off code is WKD20. Reply STOP to opt out.",
    "metadata": {
      "message_type": "marketing",
      "traffic_lane": "marketing",
      "campaign_id": "cmp_01HXV…"
    }
  }'
```

```bash cURL — transactional notice theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "channel": "sms",
    "body": "Acme: Order 98421 shipped; tracking is at https://acme.example/o/98421.",
    "metadata": {
      "message_type": "transactional",
      "traffic_lane": "transactional"
    }
  }'
```

Keep the distinction honest — the gates treat the two lanes differently by design, and a mis-tagged send either blocks legitimate transactional traffic or (worse) exempts marketing from the quiet-hours gate:

| Gate | `transactional` | `marketing` |
| - | - | - |
| Quiet-hours | Exempt — OTP and account notices flow 24/7 | Hard-gated; `422 QUIET_HOURS_BLOCKED` outside the window |
| Consent / opt-out | Opt-out still hard-blocks | Plus a marketing-eligibility consent check before dispatch |
| Double-opt-in (when enabled) | Bypassed | First send triggers the confirmation prompt |
| Fail-behaviour on a gate crash | Fail-open (the send proceeds) | Fail-closed — `503 INTERNAL_COMPLIANCE_ERROR`, retry explicitly |

A direct-API send with no lane metadata resolves to `transactional` — tag `marketing` explicitly on every promotional send rather than relying on the campaign-origin default, so the intent survives a resend outside the campaign runner.

Canonical page: [Baseline compliance posture for US SMS traffic](/guides/10dlc-marketing-baseline).

## See also

* [Baseline compliance posture for US SMS traffic](/guides/10dlc-marketing-baseline) — the conceptual guide these four recipes make runnable
* [10DLC registration](/guides/10dlc-registration) — brand and campaign filing, use-case codes, and the preflight linter
* [10DLC rejections and re-vet](/guides/10dlc-rejections-and-revet) — decode a rejection, re-vet, read throughput tiers
* [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) — the ledger the suppression pre-check reads
* [Choose your suppression entry point](/guides/suppression-three-entry-points) — CSV import versus Consent API versus Preference Center
* [Quiet hours: org-wide channel gates](/guides/quiet-hours-configuration) — per-channel windows, timezone resolution, the unknown-timezone policy
* [API error handling by example](/guides/error-handling-examples) — branch on `error.code` for every rejection shape above
* [API recipes: operations endpoints](/guides/api-recipes/operations) — the sibling cookbook for operational surfaces
