Splashify RCS

Docs

Open panel
Panel

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 with reason: "jiocx_optout" and added_by: "webhook".
  • Manual, panel operators call the endpoints below with reason: "manual". A STOP keyword reply adds reason: "keyword_stop", see Auto-replies.
http
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

http
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.

json
{
  "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)

http
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.

json
{
  "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})

http
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.

http
POST /api/v1/rcs/opt-out/import
Content-Type: multipart/form-data
  file=<opt-out.csv>

Sample CSV:

csv
phone
+919876543210
9812345678
918000000000

Response:

json
{
  "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:

json
{
  "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/send returns 403 recipient_opted_out when the recipient is on the caller's list.
  • POST /api/v1/rcs/test-send bypasses 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:

json
{
  "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.