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

# Opt-outs and STOP replies

> How Polymorfa suppresses campaign recipients, captures STOP and START replies, and tells your integration about both.

The opt-out list is your team's suppression list: numbers that campaigns must
not message. It is team-wide, so a number that opts out of one project's
campaign is suppressed in every project of that team.

Polymorfa enforces the list at two points and keeps it up to date from your
contacts' own replies.

## Where the list is enforced

| When                            | What happens                                                                                                            |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| A campaign launches             | Every recipient on the opt-out list is marked `skipped` with `lastError` `opted_out` and counted once in `skippedCount` |
| A recipient is about to be sent | A number on the list is never claimed for sending, whatever its recipient status is                                     |

Because the second check runs at send time, a number added to the list after a
campaign launched is still suppressed. Re-queuing a skipped recipient does not
send to a number that is on the list.

## Manage the list directly

```bash theme={null}
# Add one number
curl -X POST "https://api.polymorfa.com/platform/optouts" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+14155550100" }'

# Read the list
curl "https://api.polymorfa.com/platform/optouts" \
  -H "Authorization: Bearer $POLYMORFA_KEY"

# Remove one number
curl -X DELETE "https://api.polymorfa.com/platform/optouts/%2B14155550100" \
  -H "Authorization: Bearer $POLYMORFA_KEY"
```

`POST /platform/optouts/batch` takes a `phones` array and adds up to 1,000
numbers in one request. Reading needs a team API key with `campaigns:read`;
every change needs `campaigns:manage`. Project tokens are refused on opt-out
routes.

## STOP and START replies

When keyword capture is on, a contact who replies `STOP` to one of your campaign
messages is added to the opt-out list, and a contact who replies `START` is
removed from it.

A reply changes the list only when all of the following hold:

* It is a direct text message, not a group message.
* The whole message matches a configured keyword. Matching ignores letter case
  and surrounding punctuation and whitespace, so `STOP`, `stop` and `stop!`
  all match. `please stop` does not.
* The replying number received a campaign message from your team in the last
  30 days.

An opt-in keyword removes only an opt-out that a keyword reply created. An
opt-out you added by hand, by file or through the API is never removed by a
reply, so an explicit suppression stays in place.

Polymorfa compares the message body in memory and discards it. The body is never
stored, and the events below carry the keyword that matched, never the message.

## Configure the keywords

```bash theme={null}
curl "https://api.polymorfa.com/platform/optouts/settings" \
  -H "Authorization: Bearer $POLYMORFA_KEY"
```

```json theme={null}
{
  "data": {
    "enabled": true,
    "optOutKeywords": ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT",
                       "PARAR", "SAIR", "CANCELAR", "DESCADASTRAR", "BAJA"],
    "optInKeywords": ["START", "UNSTOP"],
    "updatedAt": null
  }
}
```

Those are the defaults. `updatedAt` is `null` until you save settings of your
own. `PUT` replaces all three fields at once: send every one of them, and send
nothing else, because an unrecognized field fails the request.

```bash theme={null}
curl -X PUT "https://api.polymorfa.com/platform/optouts/settings" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "optOutKeywords": ["STOP", "UNSUBSCRIBE", "PARAR"],
    "optInKeywords": ["START"]
  }'
```

| Rule              | Value                                                                       |
| ----------------- | --------------------------------------------------------------------------- |
| Keywords per list | At most 50                                                                  |
| Keyword length    | 1 to 32 characters after surrounding punctuation and whitespace are trimmed |
| `optOutKeywords`  | At least one keyword                                                        |
| Shared keywords   | The two lists must not share a keyword                                      |
| Storage           | Keywords are stored in upper case                                           |
| Propagation       | A change applies to replies within 30 seconds                               |

Set `enabled` to `false` to stop reading replies for keywords. The opt-out list
itself keeps working: existing entries stay suppressed and the API still adds
and removes numbers.

Reading settings needs `campaigns:read`; changing them needs `campaigns:manage`.
Both require a team API key.

## Webhook events

Subscribe to `contact.opted_out` and `contact.opted_in` to mirror keyword
changes in your own system.

```json theme={null}
{
  "id": "01995b51-bc80-7000-8000-000000000001",
  "session": "sales-1",
  "timestamp": "2026-09-20T10:15:30.000Z",
  "event": "contact.opted_out",
  "payload": {
    "phone": "+14155550100",
    "source": "stop-keyword",
    "keyword": "STOP",
    "session": "sales-1",
    "projectId": "0b0b6a36-33d2-4f1e-9a10-2f7a1f0d55c1"
  }
}
```

`source` is always `stop-keyword`: these two events report keyword replies, not
changes you make through the API or the Console. `keyword` is the configured
keyword that matched, in upper case. See
[Webhooks](/api/webhooks#current-event-catalog).
