> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ibubbl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Suppression & Opt-Outs

> How iBubbl handles opt-outs to keep you TCPA-compliant.

iBubbl checks opt-outs when a send is accepted. Once a contact is opted out, any new send attempt — through any API endpoint, dashboard action, or automation — fails immediately with `422 contact_opted_out`. There's no way to bypass this server-side.

> [!WARNING]
>
> **Messages accepted before the opt-out may still be sent.** The check happens when a message is accepted, and waiting messages are not withdrawn automatically today. A message that was already accepted and is still waiting — held for the contact's [sending hours](https://docs.ibubbl.com/concepts/working-hours.md), or queued for capacity — when the contact opts out can still go out afterwards. To stop it, cancel it in the dashboard (a [`message.cancelled`](https://docs.ibubbl.com/webhooks/events) webhook fires), or email [support@ibubbl.com](mailto:support@ibubbl.com).

This isn't just hygiene — it's how you stay TCPA-compliant for US recipients. Opt-out enforcement is non-negotiable.

## What triggers an opt-out

### Inbound STOP-pattern keyword

The most common path. When an inbound message body matches one of the recognized keywords (case-insensitive, exact-match after stripping whitespace), iBubbl auto-marks the contact opted out and fires a [`contact.opted_out`](https://docs.ibubbl.com/webhooks/events) webhook with `source: "auto_keyword"`.

Recognized keywords:

* `stop`
* `unsubscribe`
* `cancel`
* `end`
* `quit`
* `stopall`

The match is intentionally strict: the body must equal one of these keywords after `.strip().lower()`. Common false-positive patterns like `"STOP! that's hilarious"` or `"please stop sending"` do **not** match — they don't equal the keyword exactly.

If you need broader pattern handling, post-process inbound webhooks yourself and have your rep mark the contact opted out from the dashboard.

### Manual dashboard opt-out

A rep can mark a contact opted out from the dashboard's contact detail view. A [`contact.opted_out`](https://docs.ibubbl.com/webhooks/events) webhook fires with `source: "manual"`.

### GoHighLevel Do Not Disturb

If your account is connected to a GoHighLevel sub-account, marking a contact Do Not Disturb there (for SMS or all channels) opts them out in iBubbl too. The webhook fires with `source: "ghl_dnd"`.

### Through the API

[`POST /v1/contacts/{id}/opt-out`](https://docs.ibubbl.com/api-reference/contacts/opt-out) records an opt-out from your own system, for example one collected in your CRM, by email or on a call. The webhook fires with `source: "api"`. Calling it for someone already opted out (in any way) answers `200` and fires nothing.

> [!WARNING]
>
> **An API opt-out is as permanent as a STOP reply.** Nothing can undo it, through the API or the dashboard. Call it only for a person who asked not to be messaged. To pause messages to someone, [block](https://docs.ibubbl.com/api-reference/contacts/block.md) them instead.

## What happens after opt-out

* All `POST /v1/messages` to the contact return `422 contact_opted_out`.
* Automation (workflow) sends to the contact are refused the same way.
* The contact remains visible in your dashboard with an opted-out badge, and `opted_out` is `true` on the [contact](https://docs.ibubbl.com/api-reference/contacts/retrieve.md).
* **The opt-out belongs to the phone number, not the contact.** It matches every form of the number (`+14155551234` and `4155551234` are the same person). If you [delete the contact](https://docs.ibubbl.com/api-reference/contacts/delete.md) and the number comes back as a new contact, the new one starts with `opted_out: true` and sends are still refused.

## Re-opting in

An opt-out can't currently be undone — not through the API and not from the dashboard. This is deliberate: accidental or automated re-opt-ins are a compliance risk. If a contact was opted out by mistake, email [support@ibubbl.com](mailto:support@ibubbl.com).

## Detecting opt-outs in your integration

The cleanest path is the [`contact.opted_out`](https://docs.ibubbl.com/webhooks/events) webhook — you'll see it shortly after the opt-out happens, with the source. It fires once per person, the first time they opt out.

If you can't run webhooks, list opted-out contacts with [`GET /v1/contacts?opted_out=true`](https://docs.ibubbl.com/api-reference/contacts/list), or check `opted_out` on each contact response:

```bash cURL theme={null}
curl https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f \
  -H "Authorization: Bearer $IBUBBL_API_KEY"
```

```typescript TypeScript theme={null}
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";

const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}`, {
  headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
  signal: AbortSignal.timeout(10_000),
});

const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
if (data.opted_out) console.log("opted out at", data.opted_out_at);
```

```python Python theme={null}
import os

import requests

API_KEY = os.environ["IBUBBL_API_KEY"]

contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"

res = requests.get(
    f"https://api.ibubbl.com/v1/contacts/{contact_id}",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
if data["opted_out"]:
    print("opted out at", data["opted_out_at"])
```

```json theme={null}
{
  "id": "c8a1f4e2-...",
  "phone": "+14155551234",
  "opted_out": true,
  "opted_out_at": "2026-05-17T14:28:00.000Z",
  ...
}
```

## Block or opt out

Both stop one-to-one sends, but they are different things:

| | [Block](https://docs.ibubbl.com/api-reference/contacts/block.md) | Opt-out |
| - | - | - |
| Whose decision | Yours: you don't want to message them for now. | The contact's: they asked not to be messaged. |
| Undo | Yes: [unblock](https://docs.ibubbl.com/api-reference/contacts/unblock.md), or from the dashboard. | Never. |
| Webhook | None. | [`contact.opted_out`](https://docs.ibubbl.com/webhooks/events), once per person. |
| Sends | One-to-one sends return `422 contact_blocked`. Group conversations are not affected. | Every send returns `422 contact_opted_out`, including to a group they're in (`group_outbound_blocked_suppression`). |
| Their inbound messages | Stored and sent to you as `message.inbound`, but not marked unread, not notified, and no workflow starts. | Stored and sent to you as `message.inbound`. No workflow starts. |
| Tapbacks and typing | In a one-to-one conversation, [Add a tapback](https://docs.ibubbl.com/api-reference/messages/react.md) and [Start typing](https://docs.ibubbl.com/api-reference/conversations/start-typing.md) return `422 contact_blocked`. Removing a tapback and stopping typing still work. | The same, with `422 contact_opted_out`. |
| Read receipts | [Mark a conversation read](https://docs.ibubbl.com/api-reference/conversations/mark-read.md) marks the messages read but sends no receipt (`contact_blocked`). | The same (`contact_opted_out`). |
| After the contact is deleted | Gone: a new contact for the number isn't blocked. | Kept: a new contact for the number is opted out. |

A contact can be both. Sends then return `contact_opted_out`.

## Compliance posture

iBubbl's suppression is built for TCPA compliance for US recipients of iMessage. The defaults — strict STOP matching, mandatory enforcement, no re-opt-in — reflect that posture. If your use case has different jurisdictional requirements, email [support@ibubbl.com](mailto:support@ibubbl.com) before launching.

You remain the responsible party for your account's send patterns and content. iBubbl's enforcement reduces risk; it doesn't eliminate it.


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