Skip to main content

Read and respond to SMS spam-complaint (FBL) thresholds

Carriers track “spam report” pressure on every A2P sender — subscriber reports through the CTIA 7726 short-code channel and each carrier’s own complaint feed. When that pressure crosses a carrier’s line, the carrier filters or blocks your traffic, and you usually find out when delivery collapses. Orbit’s SMS complaint feedback loop (FBL) moves that moment earlier: complaint reports roll up per sending number into a complaint rate and a level (okelevatedhighcritical), plus a recorded sending hold you can see and override. This guide is the operator runbook: what the loop stores, how to read it, what to do at each level, and how to catch a level change before it costs you a campaign. All endpoints below are rooted at https://api.orbit.devotel.io/api/v1/numbers.
Every control on this page is yours — the reports you ingest, the campaign you pause, the sending hold you lift. Ingesting a report records complaint counts and raises or clears a hold; it never sends anything to a carrier on your behalf and never changes how your outbound path works.

How the loop works

Three endpoints make up the loop for one number:
  • POST /numbers/:id/sms-complaints/reports — ingest one complaint report: a complaint count and a message count for a period, from one source.
  • GET /numbers/:id/sms-complaints — read the current rollup: rate, level, the hold, thresholds, and the raw reports.
  • PATCH /numbers/:id/sms-complaints/suppression — hold or lift sending on the number by operator decision.
A report carries:
  • source — one of ctia, tmobile, att, verizon, uscellular, aggregator, or operator. Use operator for a report you received through your own carrier relationship and are entering by hand.
  • period_start / period_end — the window the counts cover (period_end must be on or after period_start; a bad pair is a 422).
  • complaint_count — spam complaints attributed to the number in the period.
  • message_count — messages the number sent in the period. This is the rate denominator, so send it from the same period — a complaint count against an empty denominator classifies on raw volume alone.
  • reference — optional external case id (carrier ticket, aggregator batch id) for audit trails.
Orbit keeps the last 200 reports per number — the oldest drop off as new ones arrive, so ingest per report rather than re-sending history.

How the level is computed

GET rolls the stored reports into a window (default: last 30 days) and classifies: Two rules temper the rate math:
  • Sample guard. Below 500 messages in the window the rate is too noisy to trust, so classification falls back to the absolute complaint-count floors — a low-volume number with 30 complaints still reads high.
  • Volume backstop. 100 or more complaints classifies as critical no matter how large the denominator; carriers act on raw complaint volume, not only the ratio.

The suppression hold

When the level crosses critical, Orbit records an auto suppression hold on the number (suppression.suppressed: true, mode: "auto") with the rate, level, and timestamp that triggered it. The hold is a recorded, dashboard-visible posture — the explicit signal to pause the campaign on that number. It does not silently black-hole traffic; your dispatch decides to honor it, and the change is audit-logged. The hold lifecycle:
  • Auto-set at critical, unless an operator already lifted it manually.
  • Self-clears when a later rollup recovers below critical (auto mode only).
  • Manual states stick. A hold or a lift you set through the PATCH endpoint is mode: "manual" — a later auto pass updates the level but never overrides your decision.
The same data is on the number’s Compliance tab in the dashboard — rollup, hold, and the lift control — so an operator does not need the API to clear a hold after remediation.

Read the loop

How to read it:
  • monitored: false means no reports have ever been ingested for this number — the loop is silent, not green. rollup.level will read unknown. Start feeding reports (below) before you read anything into it.
  • complaint_rate is the trend to watch when sample_sufficient is true. A number sitting at 0.08% with level: "ok" is healthier than the same level at 0.09% climbing week over week — the level tells you where you are; the rate tells you where you are heading.
  • sample_sufficient: false downgrades the rate. With a thin denominator, go by complaint_count against the absolute floors in thresholds, and treat the rate as directional only.
  • auto_suppress_recommended: true is the point where the platform believes sending should stop — act on it even if the hold has not been applied yet.
  • A non-null suppression is current state, not history. since is when the current posture began; updated_by names the operator on a manual change. reason carries the trigger (“Complaint rate 0.62% crossed the critical threshold …”) or your own note.
The thresholds block echoes the exact lines the classification used, so your alerting can compare against the shipped values instead of hardcoding them.

Response playbook

elevated — audit before you throttle

An elevated number is still deliverable, so the move is correction, not restriction:
  1. Audit opt-in provenance. Pull a sample of the complaining period’s audience and check each contact’s consent record — when and how they opted in, and whether your send matched that scope. See Consent Management. Complaint spikes after a list import or a new acquisition channel almost always trace back to provenance.
  2. Audit the content. Read the actual copy that went out in the period: does it identify you in the first line, is it what the recipient signed up for, does every message carry a working opt-out? Complaint rates punish “technically consented but feels like spam” the same as real spam.
  3. Verify opt-out handling. Confirm STOP keywords are honored and landing on your suppression lists — recipients who cannot escape report instead. See Opt-out & suppression lists.
  4. Watch the trend before acting further. If the next report window still climbs after the fixes, treat the number as high.

high / critical — stop the damage, then remediate

  1. Pause the campaigns. Halt every campaign sending from the affected number. At critical the auto hold is already recorded — honor it.
  2. Scrub the audience. Re-run your list against DNC and your own suppression layer before any resume; complaint-sourced numbers belong on suppression permanently. See Batch DNC pre-flight scrubbing and Opt-out & suppression lists.
  3. Find the source. Slice recent sends by campaign, list, and template against the report periods. A spike tied to one list or one template is a targeting or copy problem; a spike spread across everything is a provenance problem on the whole number.
  4. Rotate the sender. Move surviving traffic to a healthy number or pool while the affected number cools — see Sender pools. A number at critical that keeps sending is how a complaint problem becomes a carrier block on the whole account.
  5. Remediate, verify, then lift. Keep the hold until new report windows show the level recovered. An auto hold clears itself below critical; a manual hold only clears when you lift it:
    Resume at reduced volume and watch the next report window before returning to full throughput.
A manual lift does not make the number healthy — it makes the platform stop reminding you. If you lift while the rollup is still critical, the level stays critical and the next ingested report that crosses the line recommends suppression again. Fix the cause first; lifting is the last step, not the first.

Preventive posture

The cheapest complaint is the one that never happens:
  • Only send to provable opt-ins, and keep the proof. Every contact in a campaign audience should trace to a consent record matching what you send them. Consent Management.
  • Honor opt-outs the first time, everywhere. STOP keywords, contact DNC flags, and list suppression all feed the same send-time gates — a recipient who told you to stop and still gets a text is a complaint waiting to be filed. Opt-out & suppression lists, Send gates.
  • Scrub before every launch, and re-scrub audiences you have held for more than a couple of days. Batch DNC pre-flight scrubbing.
  • Respect quiet hours. A message that lands at 2 AM reads as spam to the recipient even when it is perfectly consented. Quiet hours configuration.
  • Cap frequency. Repetition is the other half of “feels like spam” — per-recipient caps keep one engaged subscriber from becoming five complaint reports. Frequency caps.
  • Filter inbound abuse on shared numbers. If a number receives inbound spam or abusive traffic, the per-DID inbound SMS content filter (keyword / glob-pattern / profanity rules on the inbound route) drops junk before it fans out to your webhook, agent, or auto-reply — keeping that traffic out of your agent workflows. See Inbound message routing for the routing surface the filter rides on.

Feed and monitor the loop

The loop only sees what you ingest. Wire it into operations:
  1. Feed every complaint channel. Whenever a carrier, aggregator, or your own carrier relationship hands you a per-period complaint report, POST it — with the matching message count so the rate is real:
    The response (201) returns the recomputed rollup immediately, so a feed job can alert on data.rollup.level in the same pass.
  2. Poll on a schedule (GET) and track rollup.level per sending number. Level transitions are the alert-worthy events — especially upward moves and any appearance of a non-null suppression.
  3. Route transitions to on-call. A move to high or critical, or a newly set hold, is deliverability-severity — push it to your on-call or incident channel. See On-call alerting for wiring notifier webhooks into your paging stack, and the dashboard notifications guide for surfacing the change to operators in the app.
  4. Review monthly even when green. Compare complaint_rate trends across your sending numbers; a slow drift from 0.02% to 0.09% is the early warning the level thresholds intentionally do not give you.
Every ingest and every suppression change is audit-logged (number.sms_complaint_ingested, number.sms_complaint_suppression_updated), so a post-incident review can reconstruct who knew what, when — see the Audit log guide.

Troubleshooting

monitored: false and level: "unknown" on a number you believe has reports. Nothing has been ingested for that number id. Check that your feed job posts to the correct pn_… id and got HTTP 201 back; a 422 on ingest (bad period pair or off-enum source) means no report landed. 422 on POST .../reports. The response lists the failing fields in error.details. Usual cause: period_end earlier than period_start, a source outside the seven accepted values, or a non-integer count. suppression.mode: "manual" never changes on its own. Expected — a manual hold or lift is an operator decision the auto pass respects. Re-read the number, confirm the current rollup.level, then PATCH the state you want with a reason describing the remediation. The hold cleared but the level is still critical. An auto hold self-clears only when a new ingested report recomputes the rollup below critical. If report feeding has stopped, the last-known level stands. Keep the feed running — recovery cannot be observed on stale data.
See also: