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

# Deactivation-Scrub List Hygiene Runbook: Integrate the Carrier Feed Into Your Churn Lifecycle

> Assemble the deactivation scrub into a number-release → scrub → reverify lifecycle per contact, pre-send against the carrier feed, keep the suppression ledger clean, and verify the gate without burning live coverage.

# Deactivation-Scrub List Hygiene Runbook: Integrate the Carrier Feed Into Your Churn Lifecycle

A list decays the day it is exported. Subscribers cancel lines, let ports
lapse, and carriers reclaim and reassign numbers — so by campaign day, a
fraction of any audience points at strangers or at dead lines. The
[deactivation scrub](/compliance/deactivation-scrub) gives you the carrier
disconnect feed as an API; this runbook assembles it into a lifecycle you
run per contact: **release a number → scrub its new state → reverify
before the next send → repeat**, instead of treating scrubbing as a
one-time pre-flight.

Everything here is a **tenant-owned control you configure and own**:
Orbit provides the feed, the API, and the send-path guards; whether you
opt in, what you do with a `deactivated` verdict, and how you pace
reverification are your organization's choices. This page is **not legal
advice** — confirm your obligations with qualified counsel.

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

***

## 1. What the deactivation scrub does

The scrub answers one operational question: **did the subscriber's carrier
deactivate this number since you last had a valid relationship with them?**
Numbering carriers publish a daily feed of deactivated or disconnected
numbers; Orbit ingests it centrally and exposes it as:

* `GET /deactivations/check` — one E.164 number, verdict +
  `should_scrub` boolean.
* `POST /deactivations/scrub` — up to 1,000 numbers per request, the
  pre-campaign batch leg.
* `PUT /deactivations/settings` — the per-organization opt-in
  (`deactivation_scrub_enabled`), owner/admin-only.

Every response carries `feed_synced`: while no carrier snapshot is loaded,
every verdict degrades to `no_data` and `should_scrub: false` — read it
before you trust a clear verdict. The full field matrix, the send-path
guards (`MESSAGING_NUMBER_DEACTIVATED` on SMS/MMS,
`VERIFY_NUMBER_DEACTIVATED` on OTP), and the fail-open caveat are covered
on the [reference page](/compliance/deactivation-scrub); do not duplicate
that reading here — this runbook is the *operating* layer.

## 2. Integrate it into the churn lifecycle

Scrub once at import and you ship a stale list next quarter. Wire the
three gates so lifecycle state changes on a contact always go through the
feed:

1. **Number-release sweep.** When your own inventory releases a sender or
   a destination record ages past its revalidation window (for example a
   contact not messaged in 90 days), queue the number for a
   `deactivations/check` before its next inclusion in any audience.
2. **Scrub on campaign assembly.** Before a campaign audience is frozen,
   run `POST /deactivations/scrub` in 1,000-number pages with a
   `last_seen` equal to the date you last validated each relationship
   (typically the most recent consent or successful two-way interaction).
   Drop every `should_scrub: true` row.
3. **Reverify per contact.** A number that came back `deactivated` is not
   lost forever — it needs a *new* relationship event: the subscriber
   re-joins, confirms the number on a fresh channel, or you record new
   prior-express consent dated after the carrier's
   `last_deactivation_date`. Re-run the check with that newer
   `last_seen`; only then does the contact re-enter lists.

The lifecycle is per contact, not per campaign: store the last check
verdict and the `last_seen` you used on the contact record, so the next
sweep only re-checks contacts whose state aged.

## 3. Pre-send gate syntax and what happens on recycled numbers

The manual check's only real decision is the `last_seen` filter:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/deactivations/check?phone=%2B14155550101&last_seen=2026-07-01" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

* **Omit `last_seen`** — any deactivation record on file scrubs the
  number. Use this for cold lists and imports, where you have no
  validated relationship date to defend.
* **Supply `last_seen`** — deactivations strictly before that date are
  ignored; only a *newer* disconnect scrubs the number. Use this on warm
  lists, with the date per contact from step 3 of the lifecycle.

Pre-answer the recycled-number question in your runbook, because it will
happen: the scrub finding a number on the feed almost always means the
carrier has already reassigned it or is about to. The send-path guard
then refuses SMS/MMS with `MESSAGING_NUMBER_DEACTIVATED` (422) and OTP
with `VERIFY_NUMBER_DEACTIVATED` (422) — that is the gate working, not a
provider error. The resolution is the reverify leg above: drop the
destination, re-establish a relationship, and let a dated consent postdate
the `last_deactivation_date`. Retrying the same send against the same
verdict never clears the block.

## 4. Avoid suppression-ledger side effects

A deactivation verdict is **not** an opt-out. Writing `deactivated`
contacts into the suppression ledger as if they were STOP replies mixes
two different ledgers with two different recovery paths:

* The [suppression ledger](/compliance/opt-out-suppression) encodes a
  *person's* instruction — STOP, unsubscribe, complaint — and is a hard
  send-gate with its own audit semantics and reversal rules.
* The deactivation feed encodes a *carrier's* network event. Its recovery
  is reverification + a newer consent date, not an unsuppress.

Keep them separate: route `should_scrub: true` rows to your contact-status
field (for example `carrier_deactivated` with the
`last_deactivation_date`), and let suppression stay reserved for genuine
opt-outs. That separation also keeps a bulk import of "recycled numbers"
from polluting the ledger your opt-out audit reads — with false positives
an auditor later has to explain away. If a deactivated subscriber *also*
opts out on the new relationship, that STOP enters suppression through
the normal keyword path — one ledger per truth.

## 5. Verify the integration

Run this checklist before you rely on the gate. None of it sends a
message, so none of it consumes campaign coverage:

1. **Settings round-trip.** `GET /deactivations/settings` returns your
   opt-in and a live `feed_synced`; on the SaaS platform the feed is
   synced centrally — if `feed_synced` is false, stop and treat every
   verdict as `no_data` until it flips.
2. **Known-unknown probe.** Check a number you have no history with,
   without `last_seen`. Expect a well-formed verdict (`deactivated`,
   `active`, or `no_data`) and a `should_scrub` that equals
   `status === "deactivated"` — never an error.
3. **Batch page-through.** Scrub a slice of a real audience, confirm
   `checked` equals your page size and `scrub_count + kept_count`
   reconciles. Verify your drop list is exactly the
   `should_scrub: true` rows.
4. **Send-path proof with one live send.** Send a single SMS to a contact
   you *know* is deactivated (from your own sweep). Expect a 422 with
   `MESSAGING_NUMBER_DEACTIVATED` and a written audit entry. That single
   deliberate refusal proves the guard fires — do not instrument this
   with bulk traffic.
5. **Lifecycle reload.** Record a fresh consent dated after the
   `last_deactivation_date`, re-check with the new `last_seen`, and
   confirm the verdict flips to `active`.

The guards are fail-open by design, so a lookup error never black-holes
a send; that also means verification is how you know the gate is actually
armed, not just absent.

## 6. Failure-mode codes

| Code | HTTP | Meaning | Runbook response |
| - | - | - | - |
| `DEACTIVATION_SCRUB_NOT_ENABLED` | 403 | The organization opt-in is off; check/scrub endpoints refuse until an owner/admin flips `deactivation_scrub_enabled`. | Turn the toggle on via `PUT /deactivations/settings`; the send-path guards stay skipped while off. |
| `MESSAGING_NUMBER_DEACTIVATED` | 422 | SMS/MMS pre-send guard refused the destination: a carrier deactivation on/after your most recent consent date. | Run the reverify leg (§2) — drop from active lists, re-establish consent postdating the disconnect. |
| `VERIFY_NUMBER_DEACTIVATED` | 422 | OTP on a phone channel refused: *any* deactivation record blocks a code (strict disconnect-presence — no consent ledger applies). | Confirm the number through another channel before re-attempting verification. |
| (`no_data` verdict) | 200 | No carrier snapshot is synced, or the lookup degraded; `should_scrub` is false for every number. | Not an error to retry — read `feed_synced`, and do not treat a clear verdict as safe-harbor while it is false. |

For the full row set see [Error Codes](/reference/error-codes) and
[Messaging pre-send gates](/troubleshooting/messaging-pre-send-gates).

***

See also:

* [Carrier Deactivation Scrub](/compliance/deactivation-scrub) — the
  reference page: field matrix, send-path guard semantics, fail-open
  caveat.
* [Send Gates](/compliance/send-gates) — how the deactivation guard
  layers with quiet hours, DNC, RND, and suppression at send time.
* [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) — the
  ledger this runbook deliberately keeps deactivation verdicts out of.
* [RND Safe-Harbor Pre-Contact Number Scrubbing](/guides/rnd-safe-harbor-preflight) —
  the consent-date safe-harbor check that complements the carrier churn
  feed.
* [DNC preflight scrub](/guides/dnc-preflight-scrub) — the registry-side
  pre-flight that answers the orthogonal "did the person ask to stop?"
  question.


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