Support · Contact · Legal
Verified RCS · SMS · MMS · Log in

RCS webhook guide

Technical RCS & Developers

Quick answer

A webhook is the HTTPS endpoint SimplyRCS posts events to as they happen: inbound messages, button taps, delivery and read receipts, revocations, bot-to-human handoffs, campaign completion and enrolment events. You register the URL with POST /v1/webhook-endpoints, verify the X-Signature header on every delivery, acknowledge with any 2xx, and do the real work after you have answered. Everything on this page is the contract as published in the public API spec; the runtime behaviour has not been exercised on this site, so treat the duplicate, bad-signature and slow-handler cases as things to test before you depend on them.

See this on your own phone: See pricing →

Registering an endpoint

POST /v1/webhook-endpoints takes a public HTTPS url and an events array. Private, loopback and link-local addresses are rejected, the URL may not carry embedded credentials, and redirects are followed only on the same origin. Unknown event names are dropped silently, and at least one known event is required. The response is the only time the endpoint secret is returned: store it immediately, because that secret keys every signature from then on. To rotate it, delete the endpoint and create it again.

Two more calls round out the management surface. PATCH /v1/webhook-endpoints/{id} changes the destination or the subscribed events, and it is also how you re-enable an endpoint: after 10 consecutive failed deliveries the endpoint is switched off (isActive: false) until you set isActive: true again. GET /v1/webhook-endpoints/{id}/deliveries lists every attempt, newest first, with the HTTP status your endpoint returned (or null on a timeout or network error), the attempt number and the X-Webhook-Delivery-Id of that attempt, which is the id to quote to support.

Verifying a delivery

Each delivery is an HTTPS POST with a JSON body. X-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the endpoint secret. Recompute it over the raw bytes before any JSON parsing or middleware reformatting, compare with a constant-time comparison, and reject anything that does not match. An unverified webhook is a way for anyone who learns the URL to inject fake opt-outs and fake inbound messages, so the check is mandatory, not a hardening step for later.

Test deliveries use the same envelope with data: { "test": true } and whatever event name you asked for (POST /v1/webhook-endpoints/{id}/test sends one synchronously and returns the result; the portal's connectivity test uses the event name test). After the signature check, acknowledge and ignore anything with data.test === true before you route on senderIdId or contactPhone. Missing routing fields on their own never mean a test. Test deliveries are not retried and time out after 5 seconds.

The envelope and the eleven events

Every event arrives as { "event", "data", "timestamp", "orgId" }, with brandId and campaignId present only for organisations that use multi-brand scoping. The events emitted today:

Event Fired when What data carries
message.received Any inbound SMS, MMS, RCS or WhatsApp message on one of your senders. Unknown numbers are auto-created as contacts first, so contactId is normally set. senderIdId, contactPhone, messageId, conversationId, contactId, channelId, channelType, direction: IN, contentJson
message.status Once per status transition on a message you sent, as receipts arrive from the carrier or the RCS platform. messageId, conversationId, previousStatus, status, channelType, plus the routing fields
message.revoked POST /v1/messages/{id}/revoke succeeded and the message was recalled from the device. messageId, conversationId, contactId, revokedAt, revokedBy (an API key id or user id)
button.clicked The inbound message is a tap on a suggested reply or action. Fired alongside message.received for the same message. buttonPayload (the postback data if the suggestion had one, otherwise its label), messageId, conversationId, contactId, channelType
handoff A bot flow reached a handoff node, the built-in agent decided to escalate, or an RCS webview postback asked for a person. The conversation stops receiving bot replies until it is handed back. conversationId, contactId, botId, channelId, reason, source (tier0_agent or rcs_webview_postback; absent for a flow node)
blast.complete No campaign recipients remain queued or processing. Counts are a snapshot; later receipts can change the totals. blastId, name, total, sent, delivered, failed
person.acquired A keyword, web form, QR code or API enrolment created or matched a contact. status is pending_confirmation while a double opt-in reply is still due. contactId, phone, campaign and list identifiers, status
person.subscribed Enrolment completed: immediately for single opt-in, after the confirmation reply for double opt-in. contactId, phone, campaign and list identifiers, consentType, subscribedAt
event.sent, event.failed, event.skipped A POST /v1/events submission produced a message, could not be sent (for example no template for the available channel), or was deliberately withheld by consent or quiet-hours rules. eventLogId, eventType, and contactId, channel, messageId or reason as applicable

The message status values are QUEUED, PROCESSING, SENT, DELIVERED, SEEN, FAILED and CANCELED. The typical sequence is QUEUED, SENT, DELIVERED, SEEN; FAILED is terminal. message.status is the delivery-receipt event: the names message.delivered, message.read and message.failed can be subscribed to but are reserved and not emitted, as are blast.completed, blast.failed, contact.created, contact.updated, contact.opted_out, session.started, session.expired, payment.succeeded and payment.failed.

Two fields deserve a note. senderIdId is the message-time SimplyRCS sender id, the same identifier you send with, and contactPhone is the other party's E.164 number at message time; either can be null when unproven, and both can be absent on older queued events or when adding them would push the body past the 256 KiB limit. Never infer a sender from their absence. And conversationId on an inbound event is the id to reply on: POST /v1/conversations/{conversationId}/messages keeps the reply in the same thread, which is what replaces polling for a gateway-style integration.

A message.received delivery for a tapped reply, with fictional values:

{
  "event": "message.received",
  "data": {
    "senderIdId": "snd_7f3a",
    "contactPhone": "+14155550142",
    "messageId": "msg_01h9",
    "conversationId": "cnv_4k2p",
    "contactId": "ct_8f21",
    "channelId": "ch_rcs_us",
    "channelType": "RCS",
    "direction": "IN",
    "contentJson": { "type": "text", "text": "Confirm" }
  },
  "timestamp": "2026-10-22T19:40:00Z",
  "orgId": "org_2b1c"
}

Snippet status: example payload built from the spec's envelope and event fields with fictional values, not a captured delivery.

A button.clicked event for the same tap arrives separately with buttonPayload: "confirm_appt_1043", so subscribe to that event when you only care about button traffic and leave contentJson parsing to the cases that need it.

Retries, duplicates and the upstream boundary

A delivery that times out or returns a non-2xx is retried up to 4 attempts in total, with exponential backoff starting at 5 seconds. Retries resend the exact same body bytes, so X-Signature is identical across every attempt of one event: use it, or a hash of the raw body, as your deduplication key. X-Webhook-Delivery-Id changes on every attempt and is for support lookups, not deduplication. Do not deduplicate on the event name plus an entity id either, because message.status legitimately fires several times for one message.

That is the whole of what the platform does on its side: sign, deliver, retry four times, log each attempt, and switch the endpoint off after ten consecutive failures. It does not queue events beyond the fourth attempt, replay them later or hold a dead-letter queue for you, so a long outage means the events of that window are gone from the stream and have to be reconciled from GET /v1/reports/messages and the conversation endpoints afterwards. Everything else is yours: a handler that validates the signature, writes the event to your own queue, returns 2xx inside a second or two, and processes asynchronously; idempotent processing keyed on the signature; sequencing on timestamp rather than arrival order, since a SEEN can arrive before the DELIVERED it follows; opt-out and consent events written to your suppression list before anything else; and a parking place for payloads that fail to parse so one malformed event cannot stall the pipeline.

Before relying on any of that in production, have an engineer run four runtime tests against a test endpoint (none has been run yet; the results belong on this page with the environment and date): send a test delivery with a wrong secret and confirm it is rejected, return a 500 and watch the four attempts land with the same signature, answer slowly and confirm the retry arrives while the first attempt is still processing, and let an endpoint fail ten times to see the auto-disable and the PATCH that revives it. The delivery log exists for exactly this.

The events and their fields are listed in RCS event types, how RCS read receipts work covers the read event, and the developer hub is where keys and callback URLs are registered.

Key facts
  • Register with POST /v1/webhook-endpoints; the secret is returned once and keys the X-Signature HMAC-SHA256 on every delivery.
  • Eleven events are emitted today; message.status carries delivery, read and failure (QUEUED, SENT, DELIVERED, SEEN; FAILED terminal).
  • Four attempts with exponential backoff from 5 seconds, identical bytes each time, so the signature is the deduplication key.
  • Ten consecutive failures switch the endpoint off until PATCH isActive: true; nothing is replayed, so reconcile from the message report after an outage.
  • Test deliveries carry data.test: true, are signed, are not retried and time out after 5 seconds.

Frequently asked

How do you deduplicate SimplyRCS webhook deliveries?

Use the X-Signature value, or a hash of the raw body, as the deduplication key. Per the published API spec, retries resend the exact same body bytes, so the signature is identical across every attempt of one event. X-Webhook-Delivery-Id changes on every attempt and is for support lookups, and message.status legitimately fires several times for one message.

How do you rotate a webhook signing secret?

To rotate the secret, delete the webhook endpoint and create it again. The response to POST /v1/webhook-endpoints is the only time the endpoint secret is returned, so store it immediately. That secret keys the X-Signature HMAC-SHA256 on every delivery from then on.

What happens to webhook events while your endpoint is down?

Events from a long outage are gone from the stream. Under the published contract, a failed delivery is retried up to 4 attempts in total, nothing is queued or replayed beyond the fourth, and the endpoint is switched off after 10 consecutive failed deliveries. Reconcile the missed window from GET /v1/reports/messages and the conversation endpoints, then re-enable the endpoint with PATCH.

← All RCS questions