Skip to main content
Telnyx sends webhooks to notify your application about RCS messaging events — delivery status updates, inbound messages, read receipts, and suggestion responses. This guide covers RCS-specific webhook payloads, how they differ from SMS/MMS, and how to handle them in your application.

Prerequisites


RCS vs SMS/MMS webhooks

RCS webhooks have significant structural differences from SMS/MMS. Understanding these is critical when building a multi-channel messaging application.
FeatureSMS/MMSRCS
Message bodypayload.text (string)payload.body.text (nested object)
Media formatpayload.media[] with Telnyx URLspayload.body.user_file with GCS URLs
Sender identifierfrom.phone_numberfrom.phone_number (inbound) or from.agent_id + from.agent_name (outbound)
Recipient identifierto[].phone_numberto[].phone_number (outbound) or to[].agent_id + to[].agent_name (inbound)
Read receiptsNot supportedmessage.read event
Suggestion responsesNot applicablebody.suggestion_response
Location sharingNot supportedbody.location
JSON schemaTelnyx messaging schemaSnake-case schema based on Google RCS API
Webhook URL sourceMessaging profile or per-requestRCS Agent config (inbound) + messaging profile (outbound status)
For SMS/MMS webhook handling, see Receiving Webhooks for Messaging.

Webhook URL configuration

RCS webhook routing differs from SMS/MMS:
Event TypeWebhook Source
Inbound messages (message.received)Configured on the RCS Agent
Outbound status (message.sent, message.finalized, message.read)Per-request URL → messaging profile URL → none

URL priority for outbound status

1

Per-request URLs

If webhook_url and webhook_failover_url are provided in the send request body, those are used.
2

Messaging profile URLs

If the messaging profile has webhook URLs configured, those are used.
3

No webhook

If neither is configured, no webhook is delivered. Events are still logged in Message Detail Records.

Webhook event types

EventDescriptionDirection
message.sentMessage sent to the upstream RCS providerOutbound
message.finalizedDelivery confirmed or failed by carrierOutbound
message.readRecipient read the message on their deviceOutbound
message.receivedInbound message received by your RCS AgentInbound

Delivery status webhooks

When you send an RCS message, Telnyx notifies you as the message progresses through each status.

Delivery status flow

Status reference

StatusDescription
queuedMessage queued on Telnyx’s side
sendingBeing sent to the upstream RCS provider
sentSent to the upstream provider
deliveredCarrier confirmed delivery to the device
readMessage read on the recipient’s device
sending_failedFailed to send to the upstream provider
delivery_failedCarrier could not deliver to the recipient
delivery_unconfirmedNo delivery confirmation received

Example: Delivery receipt payload

{
  "data": {
    "event_type": "message.finalized",
    "id": "4ee8c3a6-4995-4309-a3c6-38e3db9ea4be",
    "occurred_at": "2024-12-09T21:32:14.148+00:00",
    "payload": {
      "body": {
        "text": "Hello there!"
      },
      "completed_at": "2024-12-09T21:32:14.148+00:00",
      "cost": null,
      "direction": "outbound",
      "errors": [],
      "from": {
        "agent_id": "e4448a5c0670c2a9",
        "agent_name": "My RCS Agent"
      },
      "id": "ac012cbf-5e09-46af-a69a-7c0e2d90993c",
      "messaging_profile_id": "83d2343b-553f-4c5f-b8c8-fd27004f94bf",
      "organization_id": "9d76d591-1b7d-405d-8c64-1320ee070245",
      "received_at": "2024-12-09T21:32:13.552+00:00",
      "record_type": "message",
      "sent_at": "2024-12-09T21:32:13.596+00:00",
      "tags": [],
      "to": [
        {
          "carrier": "T-MOBILE USA, INC.",
          "line_type": "Wireless",
          "phone_number": "+13125000000",
          "status": "delivered"
        }
      ],
      "type": "RCS",
      "valid_until": "2024-12-09T22:32:13.552+00:00",
      "webhook_failover_url": "",
      "webhook_url": "http://webhook.site/af3a92e7-e150-442c-9fe6-61658ce26b1a"
    },
    "record_type": "event"
  },
  "meta": {
    "attempt": 1,
    "delivered_to": "http://webhook.site/af3a92e7-e150-442c-9fe6-61658ce26b1a"
  }
}

Read receipts

RCS uniquely supports read receipts — a message.read event is sent when the recipient opens and views your message. This is not available with SMS/MMS.

Example: Read receipt payload

{
  "data": {
    "event_type": "message.read",
    "id": "7bc4d2e1-3f89-4a12-b5c7-9e8d1a2f3b4c",
    "occurred_at": "2024-12-09T21:35:22.000+00:00",
    "payload": {
      "body": {
        "text": "Hello there!"
      },
      "direction": "outbound",
      "from": {
        "agent_id": "e4448a5c0670c2a9",
        "agent_name": "My RCS Agent"
      },
      "id": "ac012cbf-5e09-46af-a69a-7c0e2d90993c",
      "messaging_profile_id": "83d2343b-553f-4c5f-b8c8-fd27004f94bf",
      "to": [
        {
          "phone_number": "+13125000000",
          "status": "read"
        }
      ],
      "type": "RCS"
    },
    "record_type": "event"
  }
}

Handling read receipts

Use read receipts to:
  • Track engagement — Know which messages were actually read vs. just delivered
  • Trigger follow-ups — Send a follow-up if a message was delivered but not read after a threshold
  • Analytics — Calculate read rates for different message types or campaigns
  • UI updates — Show “read” indicators in your chat interface (like blue checkmarks)
Not all devices or carriers support read receipts. A missing message.read event doesn’t necessarily mean the message wasn’t read — the user may have disabled read receipts on their device.

Inbound message webhooks

When someone sends an RCS message to your agent, Telnyx delivers a message.received webhook to the URL configured on your RCS Agent.

Inbound message types

RCS supports richer inbound message types than SMS/MMS:
{
  "data": {
    "event_type": "message.received",
    "id": "b301ed3f-1490-491f-995f-6e64e69674d4",
    "occurred_at": "2024-12-09T20:16:07.588+00:00",
    "payload": {
      "body": {
        "text": "Hello from Telnyx!"
      },
      "direction": "inbound",
      "from": {
        "carrier": "T-Mobile USA",
        "line_type": "long_code",
        "phone_number": "+13125000000",
        "status": "webhook_delivered"
      },
      "id": "84cca175-9755-4859-b67f-4730d7f58aa3",
      "messaging_profile_id": "740572b6-099c-44a1-89b9-6c92163bc68d",
      "to": [
        {
          "agent_id": "e4448a5c0670c2a9",
          "agent_name": "My RCS Agent"
        }
      ],
      "type": "RCS"
    },
    "record_type": "event"
  }
}

SDK webhook handling examples

Handle RCS webhooks in your application with proper event routing, signature verification, and message type detection.
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/rcs", methods=["POST"])
def handle_rcs_webhook():
    event = request.json
    event_type = event["data"]["event_type"]
    payload = event["data"]["payload"]

    if event_type == "message.received":
        handle_inbound(payload)
    elif event_type == "message.sent":
        print(f"Message {payload['id']} sent")
    elif event_type == "message.finalized":
        handle_delivery(payload)
    elif event_type == "message.read":
        handle_read_receipt(payload)

    return jsonify({"status": "ok"}), 200


def handle_inbound(payload):
    body = payload["body"]
    sender = payload["from"]["phone_number"]

    if "text" in body:
        print(f"Text from {sender}: {body['text']}")
    elif "user_file" in body:
        file_info = body["user_file"]["payload"]
        print(f"File from {sender}: {file_info['file_name']} ({file_info['mime_type']})")
    elif "location" in body:
        loc = body["location"]
        print(f"Location from {sender}: {loc['latitude']}, {loc['longitude']}")
    elif "suggestion_response" in body:
        resp = body["suggestion_response"]
        print(f"Suggestion from {sender}: {resp['text']} (postback: {resp['postback_data']})")


def handle_delivery(payload):
    message_id = payload["id"]
    for recipient in payload.get("to", []):
        status = recipient.get("status")
        phone = recipient.get("phone_number")
        print(f"Message {message_id} to {phone}: {status}")

        if status == "delivery_failed":
            # Implement fallback — e.g., send via SMS
            print(f"Delivery failed for {phone}, triggering SMS fallback")


def handle_read_receipt(payload):
    message_id = payload["id"]
    for recipient in payload.get("to", []):
        phone = recipient.get("phone_number")
        print(f"Message {message_id} read by {phone}")
        # Update message status in your database
        # db.messages.update(message_id, read=True, read_at=datetime.utcnow())


if __name__ == "__main__":
    app.run(port=5000)

Building a unified SMS + RCS webhook handler

If your application handles both SMS/MMS and RCS, you need to normalize the different payload structures:
def normalize_message(event):
    """Normalize SMS/MMS and RCS webhook payloads into a common format."""
    payload = event["data"]["payload"]
    msg_type = payload.get("type", "SMS")

    if msg_type == "RCS":
        # RCS: body is nested object
        body = payload.get("body", {})
        text = body.get("text", "")
        media = []
        if "user_file" in body:
            file_info = body["user_file"]["payload"]
            media.append({
                "url": file_info["file_uri"],
                "content_type": file_info["mime_type"],
                "size": file_info.get("file_size_bytes"),
            })
        sender = payload.get("from", {}).get("phone_number", "")
    else:
        # SMS/MMS: body is flat
        text = payload.get("text", "")
        media = [
            {"url": m["url"], "content_type": m["content_type"]}
            for m in payload.get("media", [])
        ]
        sender = payload.get("from", {}).get("phone_number", "")

    return {
        "id": payload["id"],
        "type": msg_type,
        "sender": sender,
        "text": text,
        "media": media,
        "direction": payload.get("direction"),
    }

Webhook IP allowlist

If your server uses a firewall or ACL, allowlist the Telnyx webhook source subnet:
CIDRDescription
192.76.120.192/27Telnyx webhook delivery IPs

Best practices

Process webhooks asynchronously. Acknowledge receipt immediately and handle the event in a background job. If your server doesn’t respond within 2 seconds, Telnyx retries the webhook.
The same webhook may be delivered multiple times (up to 3 attempts). Use the event.data.id as a deduplication key to avoid processing the same event twice.
Validate the telnyx-signature-ed25519 and telnyx-timestamp headers using your account’s public key. See webhook security for implementation details.
When an RCS message fails (delivery_failed status), automatically fall back to SMS to ensure message delivery. Check the to[].status field in message.finalized events.
Read receipts provide valuable engagement data but aren’t guaranteed. Don’t make critical business logic dependent on receiving a message.read event — some users disable read receipts.

Next steps