Skip to main content

SMS complaint feedback loop

Email has a complaint feedback loop: a spam complaint lands as a webhook event, the address is suppressed, and the damage is readable the same day. SMS traditionally has no webhook equivalent — carriers aggregate handset spam complaints surfaced through the 7726 report-as-spam short code and carrier/CTIA messaging programs, and hand them to you or your messaging aggregator as periodic counts. Orbit’s SMS complaint feedback loop turns those counts into a per-DID complaint rate, classifies it against tuned thresholds, and records a suppression hold before the carrier blocks your traffic. This page explains how the loop fits together. The endpoint-by-endpoint contract lives in Number health, warming, and carrier reputation tracking; read this page before you build complaint ingest or respond to a critical event.

What’s a complaint in SMS

Two different signals get called “complaints,” and the loop distinguishes them:
  • Carrier/CTIA complaint report — a per-period aggregate count of spam complaints attributed to your DID, coming from the CTIA aggregator, a carrier feed (T-Mobile, AT&T, Verizon, US Cellular), an upstream messaging aggregator, or an operator-supplied manual report. This is what the loop ingests: source, period_start, period_end, complaint_count, message_count, and an optional reference case id.
  • Handset block — a per-recipient opt-out (STOP reply or a user-side block). That signal flows through the consent and suppression model, not the complaint loop. The complaint loop watches aggregate reputation pressure on the DID itself; the opt-out gate refuses specific recipients.
The health score’s complaint component counts complaint-coded carrier rejection error codes from your own delivery receipts — internal deliverability. The complaint loop is the third surface: the inbound, carrier-filed reputation-damage signal that never appears in your DLRs. Spam complainers do not get delivery failures; the carrier stops your traffic instead.

The complaint-rate math

Each ingested report is appended to the DID’s report set (bounded retention: the most recent 200 reports are kept). The rollup runs over a 30-day default window:
  • complaint_count and message_count are summed across in-window reports.
  • complaint_rate = complaint_count / message_count, clamped to [0, 1].
  • sample_sufficient is true once the window message count reaches 500, because below that the denominator is too thin for the rate to mean anything.
When the sample is insufficient, classification falls back to absolute complaint-count floors, and a raw volume of 100 or more complaints escalates to critical regardless of sample size — carriers act on raw complaint volume too, so a thin denominator does not excuse a swollen numerator.

The threshold ladder

Complaint rates are classified into five levels. The rate thresholds are tuned to the CTIA A2P guidance that a sustained ~0.1% complaint rate draws carrier scrutiny: The GET response echoes these thresholds in its thresholds object so a dashboard panel can render the ladder without hardcoding constants.

Suppression modes

Auto-suppression at the critical level is a recorded hold on the DID, not a carrier call and not a send-path block: the hold is stamped onto the DID and surfaced on the read route and dashboard so an operator pauses the campaign. Suppression state has two modes:
  • auto — threshold-driven. A critical rollup stamps suppressed: true; once the level recovers below critical, an auto hold self-clears. A repeated critical ingest refreshes the level and timestamps without resetting the hold’s original since.
  • manual — operator override, set through the PATCH endpoint. A manual lift (suppressed: false) is recorded and respected: a later critical ingest does not re-flip it into an auto hold. A manual hold (suppressed: true) stays until lifted. The override takes effect only when the report surface acknowledges an id is an owned number and the POST/PATCH audit actions record who changed what.
This mirrors the platform’s compliance stance: pause-or-resume is a tenant-owned control. Orbit records and surfaces the hold so your operator acts on it; it does not silently block outbound sends.

Update cadence

The rollup is recomputed per ingest, not on a scheduler. Every POST of a report re-runs the rollup math and the suppression reconciliation, so the level, recommended auto-suppression, and hold state you read back are always computed from the full retained report set. This keeps the ingest path and the read route classifying identically — both call the same shared kernel — and means remediation (a manual lift, or new reports with zero complaints) takes effect immediately rather than at the next daily pass.

Interaction with health and throttling

The complaint loop composes with two sibling surfaces rather than replacing them:
  • Health score — the daily deliverability kernel folds delivery rate and complaint-coded error codes into a 0–100 score and health tier. Complaint-ingest output (okcritical) is a parallel, faster-moving signal on the same DID; a critical complaint level should also push you toward a poor/critical health tier within a day’s recompute. Neither surface gates sends — they inform the warming ramp and operator decisions.
  • Warming throttle — the sender warming ramp cuts its ceiling factor to 50% (holding) at poor and 25% (throttled) at critical health tier. Ingest complaint reports early in a ramp and the warming curve reacts to complaint-driven health; the complaint loop’s own hold is the immediate operator signal while the health pass catches up.

Responding to a critical event

A level reaching critical means carrier block is the credible next step, and every carrier blocked send is worse than one you voluntarily pause. Follow this playbook:
  1. Pause the campaign on the affected DID. Confirm the suppression hold the ingest recorded (suppression.suppressed: true, mode: "auto").
  2. Identify the offending traffic. Cross-reference the reported period against which campaigns ran on the DID; the report’s source and reference point at the carrier program that filed it.
  3. Fix the root cause. Wrong audience (re-verify the registered campaign use case), wrong content (canned URLs, forbidden categories per the CTIA messaging principles), or missing consent (a recipient scope the consent and suppression model would have refused before ingest was ever needed).
  4. Rotate volume. Move traffic to a healthy DID while complaints decay out of the 30-day window — use the per-DID health kernel and the number health page to pick the candidate.
  5. Lift deliberately. Once remediation is verified, lift the hold via PATCH /api/v1/numbers/:id/sms-complaints/suppression with suppressed: false and a reason. The manual lift means a subsequent critical ingest does not silently re-hold the DID — you own the decision.
  6. Watch the level recover. As complaints age out of the window, an auto hold self-clears; a manual lift is recorded and survives later critical ingests.
If complaints recur after remediation, treat the DID as burned for that program — re-register the campaign and re-warm a replacement rather than hoping the rate decays.

See also