Blue Reacher
Opt-out handling29 / 50

Opt-out handling

How Blue Reacher detects opt-outs from keywords and plain language, enforces them across every line on your account at send time, and how to sync suppression with your own systems.

Every inbound message on your account is checked for opt-out intent before it reaches your webhook. When a contact opts out, the suppression takes effect immediately and applies to your entire account. No line can message that contact again until you deliberately clear it.

This page covers what triggers an opt-out, what happens the moment one lands, the contact.opted_out and contact.resubscribed webhook events, the API for syncing suppression with your own systems, and why enforcement happens at send time instead of at list-build time.

What triggers an opt-out

Blue Reacher runs two checks on every inbound message.

Keyword match. Case-insensitive, punctuation-tolerant, whitespace-trimmed. A reply is a keyword opt-out when the whole message, after stripping punctuation and emoji, matches one of these:

KeywordAlso matches
STOPstop, Stop., STOP!, stop
STOPALLstop all, stopall
UNSUBSCRIBEunsubscribe, Unsubscribe.
ENDend
QUITquit
CANCELcancel
OPTOUTopt out, opt-out
REVOKErevoke

Plain-language match. Most people do not type STOP. They write a sentence. Every inbound reply is classified for removal intent and the contact is suppressed when the message is a removal request, whatever the wording. These suppress:

  • "please take me off your list"
  • "don't text me again"
  • "how did you get this number, remove me"
  • "stop messaging me, I'm not interested"
  • "unsubscribe me from whatever this is"
  • "keep texting and I'm reporting this"

These do not suppress:

  • "stop by the office next week" (keyword inside a sentence, not the whole message)
  • "we're looking to stop using our current vendor" (same)
  • "not interested" (a soft no, classified as a negative reply, not a removal request)
  • "who is this?"

Soft negatives arrive on the inbound webhook with the message content and leave the contact sendable. If your policy is to suppress every negative reply, do that yourself by calling the opt-out API from your reply handler.

What happens the moment a contact opts out

In the same processing run as the reply:

  1. The contact is marked opted_out: true with a timestamp.
  2. The suppression applies account-wide: every line, every campaign, every send path, including the API and any connected CRM's workflows.
  3. The contact.opted_out webhook fires.
  4. Any new send addressed to that contact is refused.

The identity being suppressed is the phone number. A contact who opts out on one of your lines cannot be reached from any other line on the account, including lines added later, on iMessage or SMS.

The webhook events

Subscribe in the dashboard. contact.opted_out fires once per opt-out from a single enforcement chokepoint, so a STOP reply, an API opt-out and a dashboard action all emit identically:

{
  "event": "contact.opted_out",
  "event_id": "5b1f8c2d-9e47-4a03-b6d8-1c7e9f2a4b60",
  "timestamp": "2026-08-31T07:41:22.310Z",
  "api_version": "2026-08-31",
  "data": {
    "contact_id": "c9b41d68-3a2f-4e07-b5d1-6f8e2a9c4d17",
    "phone_number": "+15125550142",
    "opted_out_at": "2026-08-31T07:41:22Z",
    "source": "receive-message"
  }
}

source names the enforcement path (an inbound reply, the API, a CRM sync). Write the suppression to your system of record in the same run, because this event is the only push signal you get, and deduplicate on event_id, because delivery is at-least-once.

The mirror event, contact.resubscribed, fires only when a genuinely opted-out contact sends a recognized START/UNSTOP re-subscribe phrase; a stray "start" in an ordinary conversation does not emit it. If you suppress work in your own system on opt-out, this is the event that deliberately undoes it.

Syncing suppression from your systems

When someone unsubscribes by email, tells a rep on a call, or gets flagged Do Not Contact in your CRM, push it in:

curl -X POST https://api.bluereacher.com/v1/opt-out \
  -H "Authorization: Bearer brk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+15125550142", "opted_out": true }'
{ "success": true, "phone": "+15125550142", "opted_out": true, "already_opted_out": false }

Idempotent: repeating the call returns already_opted_out: true. The contact must already exist; unknown phones return 404 contact_not_found rather than silently creating a record, so create the contact first via POST /v1/contacts when importing a suppression file.

Read suppression state before an import or a send window with GET /v1/opt-out?phone=+15125550142, which returns opted_out, opted_out_at and known_contact. known_contact: false with no opt-out on file is not consent; it only tells you this number has never asked you to stop.

Clearing an opt-out

Re-subscribing is deliberately harder than suppressing:

curl -X POST https://api.bluereacher.com/v1/opt-out \
  -H "Authorization: Bearer brk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+15125550142", "opted_out": false, "confirm_resubscribe": true }'

Without confirm_resubscribe: true the call is refused, and a successful re-subscribe never fires a webhook. Do it only when the contact explicitly asked to hear from you again, and keep your own record of that consent; Blue Reacher does not evaluate it. You own it.

Why enforcement happens at send time

Filtering at list-build time is the obvious design: clean the list, then send it. It breaks in four predictable ways.

Lists go stale. An opt-out that lands at 10:02 does nothing to a list filtered at 09:00. The larger your campaign, the longer the window between build and last send.

Opt-outs are account-wide, lists are not. A contact who opts out on one line is usually sitting in three other lists for other campaigns. Filtering per list means every list has to learn about every opt-out independently. One missed sync is a message you cannot take back.

Scheduled and multi-step sends fire later. Follow-ups, drips, and retries execute hours or days after the list was assembled. A build-time filter cannot see a decision the contact makes in between.

Failure directions are not equal. A list-time filter that misses someone fails open: the message ships and you learn about it from the recipient. A send-time check fails closed: the send is refused and you see it in your logs. A refused send is recoverable. A delivered one is not.

Send-time enforcement also preserves the record. Deleting suppressed people from your list destroys your evidence that you honored the request. Keeping the contact with opted_out and opted_out_at means you can show exactly when someone asked you to stop and what happened next.

Checklist

  • Handle contact.opted_out and write to your system of record in the same run. Deduplicate on event_id.
  • Treat a refused send to an opted-out contact as terminal. No retry, no rerouting through another line.
  • Check GET /v1/opt-out on imported lists, and push CRM, email and phone unsubscribes into POST /v1/opt-out on a schedule, not only at import time.
  • Never clear an opt-out without a consent record you can produce on request.

On this page