Skip to Content
WebhooksRCS Incoming Events

RCS Incoming Events

Every Route Mobile callback for a customer that is RCS-Active under your partner account is forwarded to your account-level Webhook URL (set under Settings → Webhook URL on the partner panel).

There are two sources of these callbacks:

  • Inbound messages — the end-user sent a text, media file, location, or tapped a suggestion.
  • Status updates — a message you sent via POST …/customers/{customer_id}/rcs/send changed state (sent → delivered → read, or failed).

Inbound events are routed to your partner account based on the customer the message was sent to. You only ever see events for customers under your account — there is no cross-tenant leakage.

Event types

EventWhenRM event_type
rcs.message.textEnd-user replied with texttext_message
rcs.message.mediaEnd-user sent image / video / PDF / other filemedia_message
rcs.message.locationEnd-user shared a locationlocation_message
rcs.suggestion.responseEnd-user tapped a suggestion (reply / URL / dial / calendar / location)response
rcs.status.sentMessage you sent was accepted by the recipient’s carrierstatus + SENT
rcs.status.deliveredMessage was delivered to the recipient’s devicestatus + DELIVERED
rcs.status.readRecipient opened the messagestatus + READ
rcs.status.failedMessage could not be deliveredstatus + FAILED
rcs.status.unknownRM emitted a message_status we don’t recognise — surface verbatimstatus + other

Envelope

Same envelope as the rest of the account-level webhooks (production access, account deletion, RCS KYC, etc.):

{ "event_type": "rcs.message.text", "partner_id": "11111111-2222-3333-4444-555555555555", "occurred_at": "2026-05-20T08:26:32.082Z", "data": { "customer_id": "8f3b2a1e-49d8-4c2d-9e1a-b7e6d5f8a3c2", "bot_name": "SplashifyPro_promo", "rm_event": { /* unmodified Route Mobile payload — see below */ } } }

data.rm_event is the raw body Route Mobile delivered to us. The normalised fields we extract for routing live alongside (customer_id, bot_name), but everything else is in rm_event for partners who want to read RM’s payload directly without us getting in the way.

Inbound payloads

rcs.message.text

{ "event_type": "rcs.message.text", "partner_id": "...", "occurred_at": "2026-05-20T08:26:32.082Z", "data": { "customer_id": "...", "bot_name": "SplashifyPro_promo", "rm_event": { "bot_name": "SplashifyPro_promo", "event_type": "text_message", "request_id": "MxbipjUYLMRD27ZaJRMaSKgA2", "session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5", "text_message": "Hi", "timestamp": "2026-05-20T08:26:32.080697Z", "user_contact": "+919876543210", "username": "demo" } } }

rcs.message.media

rm_event.media_type is one of image, video, application (documents). media_uri is a temporary Google RCS blob URL — fetch within minutes if you want to persist the file.

{ "event_type": "rcs.message.media", "partner_id": "...", "occurred_at": "2026-05-20T08:27:44.448Z", "data": { "customer_id": "...", "bot_name": "SplashifyPro_promo", "rm_event": { "bot_name": "SplashifyPro_promo", "event_type": "media_message", "media_name": "5023611655770832826.jpg", "media_size": "67541", "media_type": "image", "media_uri": "https://rcs-copper-ap.googleapis.com/blob/...", "request_id": "MxWpHXTmckS3GRVTVa6VMydg2", "session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5", "timestamp": "2026-05-20T08:27:44.445575Z", "user_contact": "+919876543210", "username": "demo" } } }

rcs.message.location

{ "event_type": "rcs.message.location", "partner_id": "...", "occurred_at": "2026-05-20T08:28:25.144Z", "data": { "customer_id": "...", "bot_name": "SplashifyPro_promo", "rm_event": { "bot_name": "SplashifyPro_promo", "event_type": "location_message", "latitude": "12.972652", "longitude": "77.7110861", "request_id": "Mx93B8zFOdSzCySLkCcHxxIQ2", "session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5", "timestamp": "2026-05-20T08:28:25.142098Z", "user_contact": "+919876543210", "username": "demo" } } }

rcs.suggestion.response

End-user tapped one of the suggestions you attached to a previous message. rm_event.suggestion_type is reply, action (URL or dial), or view_location. The response_postback matches the postback you sent with the suggestion — use it as a stable correlation key.

{ "event_type": "rcs.suggestion.response", "partner_id": "...", "occurred_at": "2026-05-20T10:49:08.724Z", "data": { "customer_id": "...", "bot_name": "SplashifyPro_promo", "rm_event": { "bot_name": "SplashifyPro_promo", "event_type": "response", "request_id": "MxU4UwnudnTuuexqE6ZSYeDQ2", "response_postback": "track", "response_text": "Track order", "session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5", "suggestion_type": "action", "timestamp": "2026-05-20T10:49:08.720679Z", "user_contact": "+919876543210", "username": "demo" } } }

Status payloads

Status events reference the message_id you got back from the send call. Use it as your join key against your own outbound table.

rcs.status.sent / delivered / read

{ "event_type": "rcs.status.delivered", "partner_id": "...", "occurred_at": "2026-05-25T12:58:13.683Z", "data": { "customer_id": "...", "bot_name": "SplashifyPro_promo", "rm_event": { "bot_name": "SplashifyPro_promo", "event_type": "status", "message_id": "61c11dfe-5839-11f1-a96f-0a58a9feac021", "message_status": "DELIVERED", "timestamp": "2026-05-25T12:58:13.683308Z", "user_contact": "+917014311953", "username": "EvolveRCS", "extra": "lifecycle_trial_expired" } } }

sent and read payloads are identical apart from the event_type and the rm_event.message_status field.

About rm_event.extra — your passthrough field

Whatever string you passed in the extra field of the original Send RCS call comes back here verbatim on every status callback. We don’t interpret, modify, or truncate it. Common uses:

  • Idempotency key — set extra to a UUID per logical send; if network retries cause the same logical message to send twice, you see the same extra on both delivery callbacks and dedupe on your side.
  • Lifecycle tag — set extra to your internal campaign / segment name (e.g. lifecycle_trial_expired, cart_abandon_24h). Pivot delivery rates by tag without a separate join table.
  • Trace ID — set extra to the trace ID from your job system, so the callback lands in the same trace as the originating send.

If you don’t send extra, the field is omitted from the callback.

rcs.status.failed

{ "event_type": "rcs.status.failed", "partner_id": "...", "occurred_at": "2026-05-20T08:12:12.903Z", "data": { "customer_id": "...", "bot_name": "SplashifyPro_promo", "rm_event": { "bot_name": "SplashifyPro_promo", "event_type": "status", "failure_reason": "user is not in a conversation and provided message template is not approved", "message_id": "faea56dc-2b1a-11f0-a4b9-0a58a9feac02", "message_status": "FAILED", "timestamp": "2026-05-20T08:12:12.900Z", "user_contact": "+919876543210", "username": "demo", "extra": "cart_abandon_24h" } } }

rm_event.extra is also echoed on failure callbacks, so your idempotency / lifecycle / trace tagging works end-to-end even when the send doesn’t reach the recipient.

failure_reason is the carrier-side reason. Common values:

  • “user is not in a conversation and provided message template is not approved” — recipient hasn’t opted in / your bot is sandboxed.
  • “user device does not support RCS” — fall back to SMS / WhatsApp.
  • “invalid recipient number” — your phone_no was rejected.

Headers

Same headers as every account-level webhook:

HeaderValue / purpose
Content-Typeapplication/json
User-AgentSplashify-Pro-Webhook/1.0
X-Splashify-Eventrcs.message.text / rcs.status.delivered / …
X-Splashify-Delivery-IDUUID per delivery attempt — idempotency key
X-Splashify-TimestampUnix seconds — replay protection

Signature verification is not emitted on this surface today. Pin the source by hostname (api.splashifypro.com) if you need stricter auth.

Node handler example

app.post("/webhooks/splashify", express.json(), async (req, res) => { const event = req.headers["x-splashify-event"]; const { data } = req.body; switch (event) { case "rcs.message.text": await store.inbound({ customer_id: data.customer_id, from: data.rm_event.user_contact, text: data.rm_event.text_message, session_id: data.rm_event.session_id, }); break; case "rcs.suggestion.response": // Use the postback you sent originally to route the reply await store.handleSuggestion(data.rm_event.response_postback, data); break; case "rcs.status.sent": case "rcs.status.delivered": case "rcs.status.read": await store.updateOutbound(data.rm_event.message_id, event.split(".").pop()); break; case "rcs.status.failed": await alerts.failedRCS(data.rm_event.message_id, data.rm_event.failure_reason); break; } res.status(200).end(); });

Delivery semantics

  • Fire-and-forget. Route Mobile gets a 200 OK from us regardless of whether your endpoint succeeds. A failed delivery doesn’t queue a retry — RM doesn’t know about your endpoint.
  • No retries on this surface. 4xx/5xx from your endpoint is logged and dropped. Keep the endpoint healthy.
  • 10 s timeout. If your endpoint doesn’t respond in 10 s the request is cancelled. Acknowledge fast, process async.
  • Order is not guaranteed. sent, delivered, and read for the same message_id can arrive out of order. Always trust the latest rm_event.timestamp.

Idempotency

X-Splashify-Delivery-ID is unique per HTTP delivery attempt. For the underlying event, use:

  • inbound messages: rm_event.request_id (RM’s own dedupe key)
  • status events: (rm_event.message_id, event_type)