Opt-out list
Opt-out is per bot. Phones suppressed for a bot are silently dropped from
every future broadcast on that bot, and single sends (/messages/send)
reject with 403 recipient_opted_out before anything is sent.
Opting out is something a person does to a brand. Telling Acme Support to
stop does not mean Acme Deliveries may not write, and the carrier does not treat
the two agents as the same sender either. Pass bot_id on every call below.
There is also an older account-wide list
Suppressions recorded before opt-out became per bot live on an account-wide list, and they still suppress on every bot. Nothing was migrated across, because every row in that list is a person who asked to stop being messaged and re-keying the data risks losing one. A send is blocked if the phone appears on either list, so the change only ever narrows future suppressions, never past ones.
Omitting bot_id reads and writes that account-wide list. A bot-scoped DELETE will not remove an
account-wide entry: it applies to senders the request said nothing about, so clearing one is a
deliberate account-wide call with no bot_id.
The list auto-populates from two sources:
- Delivery reports, a send that fails because the recipient opted
out (
User has opted out) is authoritative. We add the recipient withreason: "jiocx_optout"andadded_by: "webhook". - Manual, panel operators call the endpoints below with
reason: "manual". A STOP keyword reply addsreason: "keyword_stop", see Auto-replies.
GET /api/v1/rcs/opt-out
POST /api/v1/rcs/opt-out
POST /api/v1/rcs/opt-out/import
DELETE /api/v1/rcs/opt-out/{phone}Access
List
GET /api/v1/rcs/opt-out?bot_id=<uuid>&limit=100&cursor=<state>Cursor-paginated. limit defaults to 100, max 500.
bot_id selects which list to read. Omit it for the account-wide one. Each row
carries a scope of "bot" or "account" so you can tell which list an entry
came from, and the summary counts everything that would suppress a send to the
selected bot, both lists together.
{
"success": true,
"opt_outs": [
{
"phone": "+919876543210",
"reason": "jiocx_optout",
"added_at": "2026-07-14T09:12:03Z",
"added_by": "webhook"
}
],
"next_cursor": "AAI…",
"summary": {
"total": 4823,
"by_reason": {
"jiocx_optout": 4801,
"manual": 22,
"keyword_stop": 0
}
}
}summary counts the whole list on every call, which is quick at
typical opt-out sizes (thousands, not millions).
Add manual (POST /rcs/opt-out)
POST /api/v1/rcs/opt-out
{ "bot_id": "48b21f9a-...", "phones": ["+919876543210", "9812345678"] }bot_id records the suppression against one sender. Omitted, it writes to the
account-wide list, which suppresses on every bot. A bot_id belonging to
another account is rejected with 400.
Every phone runs through the same E.164 normaliser as /messages/send.
phones accepts up to 500 entries per call; use POST /rcs/opt-out/import for larger batches.
{
"success": true,
"inserted": 2,
"duplicates_skipped": 0,
"malformed_rows": []
}added_by is stamped with the caller's email from the panel JWT.
Delete (DELETE /rcs/opt-out/{phone})
DELETE /api/v1/rcs/opt-out/+919876543210?bot_id=<uuid>Idempotent, a delete on a phone that isn't on the list still returns 200. Path
phones are auto-normalised.
Removes the phone from the selected bot's list only. An account-wide entry
is left in place: it applies to senders this request said nothing about, so
dropping it here would opt the person back in on brands they never contacted
you about. Clearing one is a call with no bot_id.
Import CSV (POST /rcs/opt-out/import)
Single-column CSV of phones. Same normalisation, dedup, and size caps as the
contact-list import (10 MB / 50 000 rows; 413 above).
Send bot_id as a form field alongside file, since this endpoint is
multipart rather than JSON. Omitted, the import lands on the account-wide list.
POST /api/v1/rcs/opt-out/import
Content-Type: multipart/form-data
file=<opt-out.csv>Sample CSV:
phone
+919876543210
9812345678
918000000000Response:
{
"success": true,
"inserted": 4890,
"duplicates_skipped": 12,
"malformed_rows": [{"row": 41, "phone": "abc", "reason": "not a valid Indian mobile number"}],
"elapsed_ms": 2130
}Broadcast integration
When a broadcast is created (status = preparing), its recipients
are prepared from the contact list, and every number on the bot's
opt-out list, or on the account-wide list, is left out.
The broadcast's summary counter optout_skipped_count records how
many were dropped. Broadcast detail responses surface it alongside
sent_count / delivered_count:
{
"broadcast_id": "…",
"total_count": 100000,
"sent_count": 99923,
"delivered_count": 88012,
"optout_skipped_count": 77
}Broadcasts already in flight (running, scheduled) do NOT re-scan
opt-outs mid-run, the filter is a snapshot at prep time.
Per-message send behaviour
Every send path checks the opt-out list before sending:
POST /api/v1/rcs/messages/sendreturns403 recipient_opted_outwhen the recipient is on the caller's list.POST /api/v1/rcs/test-sendbypasses the opt-out list, the target is always the operator themselves.
See the errors reference for the full 4xx/5xx code table.
SSE
New rows publish opt_out.added events on the panel event stream:
{
"type": "opt_out.added",
"data": { "phone": "+919876543210", "reason": "jiocx_optout" }
}The /opt-out page consumes this to re-render without polling. See
the dashboard stats reference for the
generic SSE hook.