# Telnyx Messaging: RCS — Full Documentation > Complete page content for RCS (Messaging section) of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this subsection: https://telnyx-openapi-ng.s3.us-east-1.amazonaws.com/llms/messaging/rcs.txt ## Getting Started ### RCS Getting Started > Source: https://developers.telnyx.com/docs/messaging/messages/rcs-getting-started.md Send rich, interactive messages with RCS (Rich Communication Services). This guide covers the setup process, approval timeline, and how to send your first RCS message. ## What is RCS? RCS is a messaging protocol that delivers app-like experiences in the native messaging app—no app download required. Unlike SMS/MMS, RCS supports: - **Rich cards** with images, titles, and action buttons - **Carousels** for product showcases - **Suggested replies** for quick responses - **Read receipts** and typing indicators - **High-resolution media** (images, video, files) RCS is currently supported on Android devices. Apple announced RCS support in iOS 18. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) ## Approval process RCS requires agent registration and carrier approval before you can send messages to the general public. The process is similar to short code approval: [Contact sales](https://telnyx.com/contact-us) to start the onboarding process. Provide your brand details, use case, and sample message content. Once submitted, Telnyx moves your agent into a testing stage. During this phase, you can invite beta test numbers using the API to test your integration while waiting for carrier approval. Carriers review and approve your agent. This process typically takes **4-6 weeks**, similar to short code approval. Once approved, your RCS Agent can send messages to any RCS-capable device. **Testing during approval:** You don't have to wait for full carrier approval to start testing. Once your agent is in testing stage, you can add beta numbers and send test messages via the API. ## 1. Create a Messaging Profile Navigate to [Messaging](https://portal.telnyx.com/#/app/messaging) in the portal. Click **Add new profile**, give it a name (e.g., "RCS Profile"), and click **Save**. Copy the Messaging Profile ID—you'll need it when sending messages. ## 2. Get your API key Go to [API Keys](https://portal.telnyx.com/#/app/api-keys) and copy your API key (or create one if needed). ## 3. Send an RCS message Once your RCS Agent is in testing stage (or fully approved), you can send messages to your beta test numbers or approved destinations. Send a simple text message via RCS: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_rcs_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "text": "Hello from Telnyx RCS!" } } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.rcs.send({ agent_id: 'your_rcs_agent_id', to: '+15559876543', messaging_profile_id: 'your_messaging_profile_id', agent_message: { content_message: { text: 'Hello from Telnyx RCS!' } } }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.rcs.send( agent_id="your_rcs_agent_id", to="+15559876543", messaging_profile_id="your_messaging_profile_id", agent_message={ "content_message": { "text": "Hello from Telnyx RCS!" } } ) print(response.data) ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) response = client.messages.rcs.send( agent_id: "your_rcs_agent_id", to: "+15559876543", messaging_profile_id: "your_messaging_profile_id", agent_message: { content_message: { text: "Hello from Telnyx RCS!" } } ) puts(response) ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go/v4" "github.com/team-telnyx/telnyx-go/v4/option" ) func main() { client := telnyx.NewClient( option.WithAPIKey(os.Getenv("TELNYX_API_KEY")), ) response, err := client.Messages.Rcs.Send(context.TODO(), telnyx.MessageRcSendParams{ AgentID: telnyx.String("your_rcs_agent_id"), To: telnyx.String("+15559876543"), MessagingProfileID: telnyx.String("your_messaging_profile_id"), AgentMessage: telnyx.RcsAgentMessageParam{ ContentMessage: telnyx.RcsAgentMessageContentMessageParam{ Text: telnyx.String("Hello from Telnyx RCS!"), }, }, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", response.Data) } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.messages.RcsMessageSendParams; import com.telnyx.sdk.models.messages.RcsMessageSendResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); RcsMessageSendParams params = RcsMessageSendParams.builder() .agentId("your_rcs_agent_id") .to("+15559876543") .messagingProfileId("your_messaging_profile_id") .agentMessage(RcsAgentMessage.builder() .contentMessage(RcsContentMessage.builder() .text("Hello from Telnyx RCS!") .build()) .build()) .build(); RcsMessageSendResponse response = client.messages().sendRcs(params); System.out.println(response); } } ``` ```csharp .NET using Telnyx; var client = new TelnyxClient(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var response = await client.Messages.SendRcsAsync(new RcsMessageSendParams { AgentId = "your_rcs_agent_id", To = "+15559876543", MessagingProfileId = "your_messaging_profile_id", AgentMessage = new RcsAgentMessage { ContentMessage = new RcsContentMessage { Text = "Hello from Telnyx RCS!" } } }); Console.WriteLine(response); ``` ```php PHP 'your_rcs_agent_id', 'to' => '+15559876543', 'messaging_profile_id' => 'your_messaging_profile_id', 'agent_message' => [ 'content_message' => [ 'text' => 'Hello from Telnyx RCS!' ] ] ]); print_r($response); ``` Send a rich card with an image and action button: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_rcs_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "rich_card": { "standalone_card": { "card_orientation": "VERTICAL", "thumbnail_image_alignment": "LEFT", "card_content": { "title": "Welcome to Telnyx", "description": "Experience rich messaging with RCS", "media": { "height": "MEDIUM", "content_info": { "file_url": "https://example.com/image.jpg" } }, "suggestions": [ { "action": { "text": "Learn More", "postback_data": "learn_more_clicked", "open_url_action": { "url": "https://telnyx.com" } } } ] } } } } } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.rcs.send({ agent_id: 'your_rcs_agent_id', to: '+15559876543', messaging_profile_id: 'your_messaging_profile_id', agent_message: { content_message: { rich_card: { standalone_card: { card_orientation: 'VERTICAL', thumbnail_image_alignment: 'LEFT', card_content: { title: 'Welcome to Telnyx', description: 'Experience rich messaging with RCS', media: { height: 'MEDIUM', content_info: { file_url: 'https://example.com/image.jpg' } }, suggestions: [ { action: { text: 'Learn More', postback_data: 'learn_more_clicked', open_url_action: { url: 'https://telnyx.com' } } } ] } } } } } }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.rcs.send( agent_id="your_rcs_agent_id", to="+15559876543", messaging_profile_id="your_messaging_profile_id", agent_message={ "content_message": { "rich_card": { "standalone_card": { "card_orientation": "VERTICAL", "thumbnail_image_alignment": "LEFT", "card_content": { "title": "Welcome to Telnyx", "description": "Experience rich messaging with RCS", "media": { "height": "MEDIUM", "content_info": { "file_url": "https://example.com/image.jpg" } }, "suggestions": [ { "action": { "text": "Learn More", "postback_data": "learn_more_clicked", "open_url_action": { "url": "https://telnyx.com" } } } ] } } } } } ) print(response.data) ``` Media URLs must be publicly accessible. Supported formats include JPEG, PNG, and GIF for images. Add suggested reply buttons for quick customer responses: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_rcs_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "text": "How would you rate your experience?", "suggestions": [ { "reply": { "text": "Great!", "postback_data": "rating_great" } }, { "reply": { "text": "Good", "postback_data": "rating_good" } }, { "reply": { "text": "Could be better", "postback_data": "rating_poor" } } ] } } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.rcs.send({ agent_id: 'your_rcs_agent_id', to: '+15559876543', messaging_profile_id: 'your_messaging_profile_id', agent_message: { content_message: { text: 'How would you rate your experience?', suggestions: [ { reply: { text: 'Great!', postback_data: 'rating_great' } }, { reply: { text: 'Good', postback_data: 'rating_good' } }, { reply: { text: 'Could be better', postback_data: 'rating_poor' } } ] } } }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.rcs.send( agent_id="your_rcs_agent_id", to="+15559876543", messaging_profile_id="your_messaging_profile_id", agent_message={ "content_message": { "text": "How would you rate your experience?", "suggestions": [ { "reply": { "text": "Great!", "postback_data": "rating_great" } }, { "reply": { "text": "Good", "postback_data": "rating_good" } }, { "reply": { "text": "Could be better", "postback_data": "rating_poor" } } ] } } ) print(response.data) ``` ### Response A successful response looks like this: ```json { "data": { "record_type": "message", "direction": "outbound", "id": "40319c33-f083-4d2a-a433-a91983b41be5", "type": "RCS", "messaging_profile_id": "400199c4-6145-43d2-a471-4c459220fcae", "from": { "agent_id": "your_rcs_agent_id", "carrier": "Telnyx", "agent_name": "Your Brand" }, "to": [ { "phone_number": "+15559876543", "status": "queued", "carrier": "T-MOBILE USA, INC.", "line_type": "Wireless" } ], "encoding": "utf-8", "parts": 1, "rcs_message_category": "basic_message" } } ``` ## 4. Handle incoming messages Set up a webhook to receive customer replies. See [Receiving RCS Webhooks](/docs/messaging/messages/receiving-rcs-webhooks) for details. ## RCS vs SMS comparison | Feature | RCS | SMS | |---------|-----|-----| | Rich media | ✅ Images, video, files | ❌ Text only (MMS for media) | | Interactive buttons | ✅ Suggested replies & actions | ❌ | | Read receipts | ✅ | ❌ | | Typing indicators | ✅ | ❌ | | Branding | ✅ Logo, colors, verified sender | ❌ | | Character limit | None | 160 (GSM-7) | | Carrier registration | Required | 10DLC/Toll-free required | ## Next steps Add AI-powered responses to your RCS agent Handle incoming RCS messages Check if a number supports RCS Explore all RCS parameters --- ### RCS with AI Assistant > Source: https://developers.telnyx.com/docs/messaging/messages/rcs-ai-assistant.md Build an AI-powered RCS agent that answers customer questions automatically. By connecting your RCS agent to Telnyx AI Assistant, you get intelligent responses powered by your knowledge base—no custom backend code required. ## Why RCS + AI Assistant? Combining RCS with AI Assistant solves two problems: 1. **RCS carrier onboarding is complex** — Carriers require functional agents before approval. AI Assistant gives your agent real functionality instantly. 2. **Building conversational AI is hard** — Instead of coding your own NLP/LLM integration, AI Assistant handles it with your knowledge sources. | Traditional approach | With Telnyx AI Assistant | |---------------------|-------------------------| | Build custom webhook handler | Configure in portal | | Integrate LLM (OpenAI, etc.) | Built-in LLM | | Manage conversation state | Handled automatically | | Build knowledge retrieval | Upload docs or connect sources | | Weeks of development | Minutes to configure | ## How it works ```mermaid sequenceDiagram participant Customer participant RCS Agent participant Messaging Profile participant AI Assistant participant Knowledge Base Customer->>RCS Agent: "How do I send an SMS?" RCS Agent->>Messaging Profile: Route message Messaging Profile->>AI Assistant: Process with AI AI Assistant->>Knowledge Base: Retrieve relevant docs Knowledge Base-->>AI Assistant: Documentation snippets AI Assistant-->>Messaging Profile: Generated response Messaging Profile-->>RCS Agent: Send reply RCS Agent-->>Customer: "To send an SMS, first..." ``` The key integration point is the **Messaging Profile**, which links your RCS Agent to an AI Assistant via the `ai_assistant_id` field. ## Prerequisites - A [Telnyx account](https://telnyx.com/sign-up) - An approved RCS Agent (or one in the approval process) - Content for your AI Assistant (FAQs, docs, or knowledge base) ## 1. Create an AI Assistant Navigate to [AI Assistants](https://portal.telnyx.com/#/app/ai/assistants) in the portal. Click **Create Assistant** and give it a name (e.g., "Support Bot"). Write instructions that define your assistant's behavior: ```text You are a helpful customer support agent for [Your Company]. Guidelines: - Be friendly and professional - Keep responses concise (under 160 characters when possible) - If you don't know something, offer to connect them with a human - Always greet new customers warmly ``` Upload documents or connect to your knowledge base: - **Documents**: Upload PDFs, text files, or markdown - **Websites**: Crawl your help center or documentation - **Custom**: Connect via API for dynamic content Save your assistant and copy the Assistant ID (e.g., `assistant-11deda65-f3f0-457a-9946-ec021622b061`). ## 2. Create a Messaging Profile with AI Navigate to [Messaging](https://portal.telnyx.com/#/app/messaging) in the portal. Click **Add new profile** and name it (e.g., "RCS AI Profile"). In the profile settings, find **AI Assistant** and select your assistant from the dropdown (or enter the Assistant ID). Save the profile and copy the Messaging Profile ID. ## 3. Link your RCS Agent to the AI-enabled profile Once your RCS Agent is approved by carriers, link it to your AI-enabled messaging profile. If you already have an approved RCS Agent, update it to use your AI-enabled messaging profile: ```bash curl curl -X PATCH https://api.telnyx.com/v2/rcs_agents/YOUR_RCS_AGENT_ID \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "profile_id": "your_ai_messaging_profile_id" }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.rcsAgents.update('YOUR_RCS_AGENT_ID', { profile_id: 'your_ai_messaging_profile_id' }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.rcs_agents.update( "YOUR_RCS_AGENT_ID", profile_id="your_ai_messaging_profile_id" ) print(response.data) ``` When submitting your RCS Agent for carrier approval, request that Telnyx provisions it on your existing AI Assistant messaging profile. [Contact sales](https://telnyx.com/contact-us) with: - Your AI Assistant ID - Your Messaging Profile ID - Your RCS Agent details This way, your agent is ready to use AI responses as soon as it's approved. With a functional AI Assistant attached, your RCS agent has real capabilities to demonstrate during carrier review—making approval easier. ## 4. Test your AI-powered RCS agent Once your RCS Agent is approved (or using test numbers), send a message to trigger the AI: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_rcs_agent_id", "to": "+15559876543", "messaging_profile_id": "your_ai_messaging_profile_id", "agent_message": { "content_message": { "text": "Hello! How can I help you today?" } } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const response = await client.messages.rcs.send({ agent_id: 'your_rcs_agent_id', to: '+15559876543', messaging_profile_id: 'your_ai_messaging_profile_id', agent_message: { content_message: { text: 'Hello! How can I help you today?' } } }); console.log(response.data); ``` ```python Python import os from telnyx import Telnyx client = Telnyx( api_key=os.environ.get("TELNYX_API_KEY"), ) response = client.messages.rcs.send( agent_id="your_rcs_agent_id", to="+15559876543", messaging_profile_id="your_ai_messaging_profile_id", agent_message={ "content_message": { "text": "Hello! How can I help you today?" } } ) print(response.data) ``` When the customer replies, the AI Assistant automatically: 1. Receives the inbound message via the messaging profile 2. Processes it with your configured LLM and knowledge base 3. Sends an intelligent response back via RCS **No webhook code required**—the AI handles the conversation. ## Best practices RCS messages display on mobile devices. Configure your AI to: - Keep responses concise (under 160 characters when possible) - Use bullet points for lists - Avoid long paragraphs Configure a greeting in your AI Assistant to welcome new customers: ```text Welcome to [Company] Support! I'm here to help with questions about our products and services. What can I assist you with today? ``` Include instructions for when the AI should hand off to a human: ```text If the customer: - Asks to speak to a human - Has a billing dispute - Is frustrated after 3 exchanges Respond: "I'd be happy to connect you with our support team. Please call 1-800-XXX-XXXX or visit support.example.com" ``` Before going live, test with questions your customers actually ask: - Check your support ticket history - Review FAQ page analytics - Test edge cases and ambiguous questions ## Monitoring and analytics Track your AI Assistant's performance in the portal: - **Conversation logs**: Review AI responses and customer satisfaction - **Knowledge gaps**: Identify questions the AI couldn't answer - **Response times**: Monitor latency and throughput Navigate to [AI Assistants > Insights](https://portal.telnyx.com/#/app/ai/assistants) to access analytics. ## Pricing | Component | Pricing | |-----------|---------| | RCS messages | [See RCS pricing](https://telnyx.com/pricing/messaging) | | AI Assistant | [See AI pricing](https://telnyx.com/pricing/conversational-ai) | AI Assistant charges are based on tokens processed. Simple Q&A conversations typically cost fractions of a cent per exchange. ## Try it yourself Experience RCS + AI Assistant firsthand by chatting with our demo bot. **Requirements:** US phone number and RCS-enabled device (Android with Google Messages, or iOS 18+) {/* Deeplink URL from /v2/messages/rcs/deeplinks API - sms: scheme with @rbm.goog is the Google RCS standard */} Tap to open a conversation with our AI-powered support agent This demo uses an [RCS deeplink](/docs/messaging/messages/rcs-deeplinks) to start the conversation. The deeplink URL is generated by the Telnyx API and uses the standard Google RCS `sms:` scheme with an `@rbm.goog` address: ```bash curl -L 'https://api.telnyx.com/v2/messages/rcs/deeplinks/telnyx_support_v9d1aaax_agent' \ -H 'Authorization: Bearer ' ``` The deeplink opens your messaging app and connects you to our AI-powered support agent, which uses: - **Telnyx AI Assistant** for natural language understanding - **Knowledge retrieval** from our documentation - **RCS rich messaging** for a native chat experience ## Next steps Deep dive into AI Assistant configuration Learn RCS fundamentals Send interactive rich cards Create deeplinks for your own agents --- ## Sending Messages ### Send RCS Messages > Source: https://developers.telnyx.com/docs/messaging/messages/send-an-rcs-message.md Send rich, interactive messages using RCS (Rich Communication Services). This guide covers sending text, rich cards, carousels, and configuring SMS fallback for non-RCS devices. **New to RCS?** Start with the [RCS Getting Started](/docs/messaging/messages/rcs-getting-started/index) guide to set up your RCS agent and get approved by Google. ## Prerequisites - A verified [RCS agent](/docs/messaging/messages/rcs-getting-started/index) approved by Google - A [messaging profile](/docs/messaging/messages/messaging-profiles-overview/index) with RCS enabled - Your agent ID (from the RCS agent setup) --- ## Send a text message The simplest RCS message — plain text with optional suggested replies: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "text": "Hi! How can we help you today?" } } }' ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } response = requests.post( "https://api.telnyx.com/v2/messages/rcs", headers=headers, json={ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "text": "Hi! How can we help you today?" } }, }, ) print(f"Message sent: {response.json()['data']['id']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }; const response = await axios.post( 'https://api.telnyx.com/v2/messages/rcs', { agent_id: 'your_agent_id', to: '+15559876543', messaging_profile_id: 'your_messaging_profile_id', agent_message: { content_message: { text: 'Hi! How can we help you today?', }, }, }, { headers } ); console.log(`Message sent: ${response.data.data.id}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" uri = URI("https://api.telnyx.com/v2/messages/rcs") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" request["Content-Type"] = "application/json" request.body = { agent_id: "your_agent_id", to: "+15559876543", messaging_profile_id: "your_messaging_profile_id", agent_message: { content_message: { text: "Hi! How can we help you today?" } } }.to_json response = http.request(request) data = JSON.parse(response.body) puts "Message sent: #{data['data']['id']}" ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { payload := map[string]interface{}{ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": map[string]interface{}{ "content_message": map[string]interface{}{ "text": "Hi! How can we help you today?", }, }, } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "https://api.telnyx.com/v2/messages/rcs", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) fmt.Printf("Message sent: %s\n", data["id"]) } ``` ```php PHP 'your_agent_id', 'to' => '+15559876543', 'messaging_profile_id' => 'your_messaging_profile_id', 'agent_message' => [ 'content_message' => [ 'text' => 'Hi! How can we help you today?' ] ] ]; $ch = curl_init('https://api.telnyx.com/v2/messages/rcs'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); echo "Message sent: {$response['data']['id']}\n"; ``` --- ## Send a rich card Rich cards display media, text, and action buttons in an interactive format: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "rich_card": { "standalone_card": { "card_orientation": "VERTICAL", "thumbnail_image_alignment": "LEFT", "card_content": { "title": "Order Shipped! 📦", "description": "Your order #12345 is on its way. Expected delivery: March 15.", "media": { "height": "MEDIUM", "content_info": { "file_url": "https://example.com/shipping.jpg", "content_type": "image/jpeg" } }, "suggestions": [ { "action": { "text": "Track Order", "open_url_action": { "url": "https://example.com/track/12345" } } }, { "reply": { "text": "Contact Support", "postback_data": "support_request" } } ] } } } } } }' ``` ```python Python response = requests.post( "https://api.telnyx.com/v2/messages/rcs", headers=headers, json={ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "rich_card": { "standalone_card": { "card_orientation": "VERTICAL", "thumbnail_image_alignment": "LEFT", "card_content": { "title": "Order Shipped! 📦", "description": "Your order #12345 is on its way.", "media": { "height": "MEDIUM", "content_info": { "file_url": "https://example.com/shipping.jpg", "content_type": "image/jpeg", }, }, "suggestions": [ { "action": { "text": "Track Order", "open_url_action": { "url": "https://example.com/track/12345" }, } }, { "reply": { "text": "Contact Support", "postback_data": "support_request", } }, ], }, } } } }, }, ) print(f"Rich card sent: {response.json()['data']['id']}") ``` ```javascript Node const response = await axios.post( 'https://api.telnyx.com/v2/messages/rcs', { agent_id: 'your_agent_id', to: '+15559876543', messaging_profile_id: 'your_messaging_profile_id', agent_message: { content_message: { rich_card: { standalone_card: { card_orientation: 'VERTICAL', thumbnail_image_alignment: 'LEFT', card_content: { title: 'Order Shipped! 📦', description: 'Your order #12345 is on its way.', media: { height: 'MEDIUM', content_info: { file_url: 'https://example.com/shipping.jpg', content_type: 'image/jpeg', }, }, suggestions: [ { action: { text: 'Track Order', open_url_action: { url: 'https://example.com/track/12345', }, }, }, { reply: { text: 'Contact Support', postback_data: 'support_request', }, }, ], }, }, }, }, }, }, { headers } ); console.log(`Rich card sent: ${response.data.data.id}`); ``` ### Card options | Property | Values | Description | |----------|--------|-------------| | `card_orientation` | `VERTICAL`, `HORIZONTAL` | Card layout direction | | `thumbnail_image_alignment` | `LEFT`, `RIGHT` | Image position (horizontal cards only) | | `media.height` | `SHORT`, `MEDIUM`, `TALL` | Image display height | --- ## Send a carousel Display multiple cards in a swipeable carousel — ideal for product listings: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "rich_card": { "carousel_card": { "card_width": "MEDIUM", "card_contents": [ { "title": "Classic Burger", "description": "$8.99 — Our signature burger", "media": { "height": "MEDIUM", "content_info": { "file_url": "https://example.com/burger.jpg" } }, "suggestions": [ { "action": { "text": "Order Now", "open_url_action": { "url": "https://example.com/order/burger" } } } ] }, { "title": "Veggie Wrap", "description": "$7.49 — Fresh and healthy", "media": { "height": "MEDIUM", "content_info": { "file_url": "https://example.com/wrap.jpg" } }, "suggestions": [ { "action": { "text": "Order Now", "open_url_action": { "url": "https://example.com/order/wrap" } } } ] } ] } } } } }' ``` Carousels require a minimum of 2 cards and a maximum of 10. All cards in a carousel use the same `card_width`. --- ## Add suggested actions Suggestions appear as tappable buttons below your message: Quick reply buttons that send a predefined response back to your webhook: ```json "suggestions": [ { "reply": { "text": "Yes, confirm", "postback_data": "confirm_order_12345" } }, { "reply": { "text": "No, cancel", "postback_data": "cancel_order_12345" } } ] ``` Open a URL in the device browser: ```json "suggestions": [ { "action": { "text": "Visit Website", "open_url_action": { "url": "https://example.com" } } } ] ``` Initiate a phone call: ```json "suggestions": [ { "action": { "text": "Call Us", "dial_action": { "phone_number": "+15551234567" } } } ] ``` Open a map location: ```json "suggestions": [ { "action": { "text": "Get Directions", "view_location_action": { "lat_long": { "latitude": 40.7128, "longitude": -74.0060 }, "label": "Acme Corp HQ" } } } ] ``` --- ## SMS fallback Not all devices support RCS. Configure automatic SMS fallback for non-RCS recipients: ```bash curl curl -X POST https://api.telnyx.com/v2/messages/rcs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "text": "Your order #12345 has shipped! Track at https://example.com/track/12345" } }, "fallback": { "from": "+15551234567", "text": "Your order #12345 has shipped! Track at https://example.com/track/12345" } }' ``` ```python Python response = requests.post( "https://api.telnyx.com/v2/messages/rcs", headers=headers, json={ "agent_id": "your_agent_id", "to": "+15559876543", "messaging_profile_id": "your_messaging_profile_id", "agent_message": { "content_message": { "text": "Your order #12345 has shipped! Track at https://example.com/track/12345" } }, "fallback": { "from": "+15551234567", "text": "Your order #12345 has shipped! Track at https://example.com/track/12345", }, }, ) data = response.json()["data"] print(f"Sent via: {data['type']}") # "RCS" or "SMS" depending on device support ``` ```javascript Node const response = await axios.post( 'https://api.telnyx.com/v2/messages/rcs', { agent_id: 'your_agent_id', to: '+15559876543', messaging_profile_id: 'your_messaging_profile_id', agent_message: { content_message: { text: 'Your order #12345 has shipped!', }, }, fallback: { from: '+15551234567', text: 'Your order #12345 has shipped! Track at https://example.com/track/12345', }, }, { headers } ); console.log(`Sent via: ${response.data.data.type}`); ``` The `fallback.from` number must be a Telnyx number on your messaging profile with SMS capability. The fallback message is plain text only — rich card content is not included. --- ## RCS vs. SMS/MMS comparison | Feature | RCS | SMS | MMS | |---------|-----|-----|-----| | **Rich cards** | ✅ | ❌ | ❌ | | **Carousels** | ✅ | ❌ | ❌ | | **Suggested actions** | ✅ | ❌ | ❌ | | **Read receipts** | ✅ | ❌ | ❌ | | **Typing indicators** | ✅ | ❌ | ❌ | | **Images/video** | ✅ High-res | ❌ | ✅ Compressed | | **Character limit** | None | 160/segment | None | | **Device support** | Android (+ iOS 18) | Universal | Universal | | **Encoding** | UTF-8 | GSM-7/UTF-16 | UTF-8 | --- ## Related resources Set up your RCS agent and get approved by Google. Check device RCS capability before sending. Handle inbound RCS messages and delivery events. Full API reference for RCS messaging. --- ### Capabilities & Deeplinks > Source: https://developers.telnyx.com/docs/messaging/messages/rcs-capabilities.md Check whether a recipient's device supports RCS — and which features it supports — before sending. This lets you adapt your message format (rich card vs. plain text) and fall back to SMS when needed. ## Query single number Check RCS capabilities for a single phone number: ```bash curl curl -s https://api.telnyx.com/v2/messaging/rcs/capabilities/{agent_id}/{phone_number} \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python import os import requests API_KEY = os.environ.get("TELNYX_API_KEY") AGENT_ID = "your_agent_id" headers = {"Authorization": f"Bearer {API_KEY}"} def check_rcs_capabilities(phone_number: str) -> dict: """Check if a phone number supports RCS and which features.""" response = requests.get( f"https://api.telnyx.com/v2/messaging/rcs/capabilities/{AGENT_ID}/{phone_number}", headers=headers, ) data = response.json()["data"] return { "phone_number": data["phone_number"], "rcs_enabled": bool(data.get("features")), "features": data.get("features") or [], "supports_rich_cards": "RICHCARD_STANDALONE" in (data.get("features") or []), "supports_carousel": "RICHCARD_CAROUSEL" in (data.get("features") or []), } result = check_rcs_capabilities("+15559876543") print(f"RCS enabled: {result['rcs_enabled']}") print(f"Features: {result['features']}") ``` ```javascript Node const axios = require('axios'); const headers = { Authorization: `Bearer ${process.env.TELNYX_API_KEY}`, }; const agentId = 'your_agent_id'; async function checkRcsCapabilities(phoneNumber) { const response = await axios.get( `https://api.telnyx.com/v2/messaging/rcs/capabilities/${agentId}/${phoneNumber}`, { headers } ); const { data } = response.data; return { phoneNumber: data.phone_number, rcsEnabled: Boolean(data.features?.length), features: data.features || [], supportsRichCards: data.features?.includes('RICHCARD_STANDALONE') ?? false, supportsCarousel: data.features?.includes('RICHCARD_CAROUSEL') ?? false, }; } const result = await checkRcsCapabilities('+15559876543'); console.log(`RCS enabled: ${result.rcsEnabled}`); console.log(`Features: ${result.features.join(', ')}`); ``` ```ruby Ruby require "net/http" require "json" require "uri" api_key = ENV["TELNYX_API_KEY"] agent_id = "your_agent_id" def check_rcs(agent_id, phone_number, api_key) uri = URI("https://api.telnyx.com/v2/messaging/rcs/capabilities/#{agent_id}/#{phone_number}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{api_key}" response = http.request(request) data = JSON.parse(response.body)["data"] { rcs_enabled: !data["features"].nil? && !data["features"].empty?, features: data["features"] || [] } end result = check_rcs(agent_id, "+15559876543", api_key) puts "RCS enabled: #{result[:rcs_enabled]}" ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" "os" ) func checkRCS(agentID, phoneNumber string) ([]string, error) { url := fmt.Sprintf("https://api.telnyx.com/v2/messaging/rcs/capabilities/%s/%s", agentID, phoneNumber) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("TELNYX_API_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result struct { Data struct { Features []string `json:"features"` } `json:"data"` } json.NewDecoder(resp.Body).Decode(&result) return result.Data.Features, nil } func main() { features, _ := checkRCS("your_agent_id", "+15559876543") fmt.Printf("Features: %v\n", features) } ``` ```php PHP true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}"], ]); $data = json_decode(curl_exec($ch), true)['data']; curl_close($ch); $rcsEnabled = !empty($data['features']); echo "RCS enabled: " . ($rcsEnabled ? 'yes' : 'no') . "\n"; echo "Features: " . implode(', ', $data['features'] ?? []) . "\n"; ``` ### Response examples ```json { "data": { "record_type": "rcs.capabilities", "phone_number": "+15559876543", "agent_id": "your_agent_id", "agent_name": "Acme Bot", "features": [ "ACTION_CREATE_CALENDAR_EVENT", "ACTION_DIAL", "ACTION_OPEN_URL", "ACTION_OPEN_URL_IN_WEBVIEW", "ACTION_SHARE_LOCATION", "ACTION_VIEW_LOCATION", "RICHCARD_CAROUSEL", "RICHCARD_STANDALONE" ], "status": "Success" } } ``` ```json { "data": { "features": ["GENERIC_RCS_FEATURE"], "status": "Success" } } ``` The device supports RCS but specific features couldn't be determined. Send basic RCS text — avoid rich cards. ```json { "data": { "features": null, "status": "RCS is disabled or agent is not provisioned for the carrier" } } ``` Fall back to SMS/MMS for this recipient. ### Feature reference | Feature | Description | Use for | |---------|-------------|---------| | `RICHCARD_STANDALONE` | Single rich card support | Product cards, order updates | | `RICHCARD_CAROUSEL` | Swipeable carousel cards | Product listings, menus | | `ACTION_OPEN_URL` | Open URL button | Links to websites | | `ACTION_OPEN_URL_IN_WEBVIEW` | Open URL in webview | In-app browsing | | `ACTION_DIAL` | Phone call button | Click-to-call | | `ACTION_VIEW_LOCATION` | View map location | Directions, store locator | | `ACTION_SHARE_LOCATION` | Share user's location | Delivery tracking | | `ACTION_CREATE_CALENDAR_EVENT` | Add calendar event | Appointment booking | | `ACTION_COMPOSE` | Compose message | Message drafting | | `GENERIC_RCS_FEATURE` | Basic RCS (details unknown) | Text-only RCS | --- ## Bulk capability query Check up to 100 numbers at once to efficiently segment your audience: ```bash curl curl -X POST https://api.telnyx.com/v2/messaging/rcs/bulk_capabilities \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "agent_id": "your_agent_id", "phone_numbers": ["+15551234567", "+15559876543", "+15550001111"] }' ``` ```python Python def bulk_check_rcs(phone_numbers: list[str]) -> dict: """Check RCS capabilities for up to 100 numbers.""" response = requests.post( "https://api.telnyx.com/v2/messaging/rcs/bulk_capabilities", headers={**headers, "Content-Type": "application/json"}, json={ "agent_id": AGENT_ID, "phone_numbers": phone_numbers, }, ) results = {"rcs": [], "sms_fallback": []} for entry in response.json()["data"]: if entry.get("features"): results["rcs"].append(entry["phone_number"]) else: results["sms_fallback"].append(entry["phone_number"]) return results # Segment audience numbers = ["+15551234567", "+15559876543", "+15550001111"] segments = bulk_check_rcs(numbers) print(f"RCS capable: {len(segments['rcs'])}") print(f"SMS fallback: {len(segments['sms_fallback'])}") ``` ```javascript Node async function bulkCheckRcs(phoneNumbers) { const response = await axios.post( 'https://api.telnyx.com/v2/messaging/rcs/bulk_capabilities', { agent_id: agentId, phone_numbers: phoneNumbers, }, { headers: { ...headers, 'Content-Type': 'application/json' } } ); const results = { rcs: [], smsFallback: [] }; for (const entry of response.data.data) { if (entry.features?.length) { results.rcs.push(entry.phone_number); } else { results.smsFallback.push(entry.phone_number); } } return results; } const segments = await bulkCheckRcs(['+15551234567', '+15559876543']); console.log(`RCS: ${segments.rcs.length}, SMS: ${segments.smsFallback.length}`); ``` RCS capability queries can be **slow** (several seconds per request). For time-sensitive applications, cache results and refresh periodically rather than querying before every message. --- ## Send with automatic fallback Use capability queries to send the best possible message format: ```python Python def send_adaptive_message(to: str, title: str, body: str, image_url: str = None): """Send RCS rich card if supported, otherwise fall back to SMS.""" caps = check_rcs_capabilities(to) if caps["supports_rich_cards"] and image_url: # Send rich card requests.post( "https://api.telnyx.com/v2/messages/rcs", headers={**headers, "Content-Type": "application/json"}, json={ "agent_id": AGENT_ID, "to": to, "messaging_profile_id": PROFILE_ID, "agent_message": { "content_message": { "rich_card": { "standalone_card": { "card_orientation": "VERTICAL", "card_content": { "title": title, "description": body, "media": { "height": "MEDIUM", "content_info": {"file_url": image_url}, }, }, } } } }, "fallback": { "from": FROM_NUMBER, "text": f"{title}\n{body}", }, }, ) elif caps["rcs_enabled"]: # Send RCS text (no rich card support) requests.post( "https://api.telnyx.com/v2/messages/rcs", headers={**headers, "Content-Type": "application/json"}, json={ "agent_id": AGENT_ID, "to": to, "messaging_profile_id": PROFILE_ID, "agent_message": { "content_message": {"text": f"{title}\n{body}"} }, "fallback": {"from": FROM_NUMBER, "text": f"{title}\n{body}"}, }, ) else: # Send SMS directly requests.post( "https://api.telnyx.com/v2/messages", headers={**headers, "Content-Type": "application/json"}, json={"from": FROM_NUMBER, "to": to, "text": f"{title}\n{body}"}, ) ``` --- ## RCS Deeplinks Deeplinks let users start an RCS conversation from a website, email, or QR code — without having your number saved. ### Generate a deeplink ```bash curl # With fallback phone number and message body curl -s 'https://api.telnyx.com/v2/messages/rcs/deeplinks/{agent_id}?phone_number=%2B15554443333&body=hello%20world' \ -H "Authorization: Bearer YOUR_API_KEY" # Without fallback (RCS only) curl -s 'https://api.telnyx.com/v2/messages/rcs/deeplinks/{agent_id}' \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python import urllib.parse def generate_deeplink( agent_id: str, fallback_phone: str = None, message_body: str = None, ) -> str: """Generate an RCS deeplink URL.""" url = f"https://api.telnyx.com/v2/messages/rcs/deeplinks/{agent_id}" params = {} if fallback_phone: params["phone_number"] = fallback_phone if message_body: params["body"] = message_body if params: url += "?" + urllib.parse.urlencode(params) response = requests.get(url, headers=headers) return response.json()["data"]["url"] # Generate deeplink with fallback link = generate_deeplink( agent_id="your_agent_id", fallback_phone="+15554443333", message_body="Hi, I'd like to learn more!", ) print(f"Deeplink: {link}") ``` ```javascript Node async function generateDeeplink(agentId, fallbackPhone, messageBody) { const params = new URLSearchParams(); if (fallbackPhone) params.set('phone_number', fallbackPhone); if (messageBody) params.set('body', messageBody); const url = `https://api.telnyx.com/v2/messages/rcs/deeplinks/${agentId}${ params.toString() ? '?' + params.toString() : '' }`; const response = await axios.get(url, { headers }); return response.data.data.url; } const link = await generateDeeplink('your_agent_id', '+15554443333', 'Hi!'); console.log(`Deeplink: ${link}`); ``` ### Response ```json { "data": { "url": "sms:+18445550001?service_id=agent_id%40rbm.goog&body=hello%20world" } } ``` ### Use deeplinks Embed the deeplink in an HTML button or link. The URL won't open directly in a browser — it must be an `` tag or button click: ```html 💬 Chat with us on RCS ``` Convert the deeplink URL to a QR code for print materials, in-store signage, or business cards. Users scan with their camera to open an RCS conversation. Include the deeplink in marketing emails as a CTA button. When tapped on Android with Google Messages, it opens the RCS conversation directly. ### Requirements | Requirement | Details | |-------------|---------| | **Device** | Android with Google Messages installed | | **OS version** | `messages.android_20241029_00` or later | | **Fallback** | Use `phone_number` parameter for non-RCS devices (opens SMS instead) | --- ## Related resources Send text, rich cards, carousels, and suggested actions. Handle inbound RCS messages and delivery events. Set up your RCS agent and get approved. Full API reference for RCS messaging. --- ### RCS Deeplinks > Source: https://developers.telnyx.com/docs/messaging/messages/rcs-deeplinks.md RCS Deeplinks content has been consolidated into the [RCS Capabilities & Deeplinks](/docs/messaging/messages/rcs-capabilities/index) guide. See the **RCS Deeplinks** section for full documentation including SDK examples and usage patterns. ## Quick reference Generate a deeplink to start an RCS conversation: ```bash curl -s 'https://api.telnyx.com/v2/messages/rcs/deeplinks/{agent_id}?phone_number=%2B15554443333&body=hello%20world' \ -H "Authorization: Bearer YOUR_API_KEY" ``` Response: ```json { "data": { "url": "sms:+18445550001?service_id=agent_id%40rbm.goog&body=hello%20world" } } ``` For SDK examples, fallback configuration, and usage patterns (website buttons, QR codes, email campaigns), see: Full guide including capability queries, adaptive sending, and deeplink generation. --- ## Receiving Messages ### RCS Webhooks > Source: https://developers.telnyx.com/docs/messaging/messages/receiving-rcs-webhooks.md 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 - A [Telnyx account](https://telnyx.com/sign-up) with an [RCS Agent](/docs/messaging/messages/rcs-getting-started) - A publicly accessible HTTPS endpoint (or [ngrok](/development/development-tools/ngrok-setup) for local development) - Your [API key](https://portal.telnyx.com/#/app/api-keys) and [public key](https://portal.telnyx.com/#/app/api-keys) (for signature verification) --- ## 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. | Feature | SMS/MMS | RCS | |---------|---------|-----| | **Message body** | `payload.text` (string) | `payload.body.text` (nested object) | | **Media format** | `payload.media[]` with Telnyx URLs | `payload.body.user_file` with GCS URLs | | **Sender identifier** | `from.phone_number` | `from.phone_number` (inbound) or `from.agent_id` + `from.agent_name` (outbound) | | **Recipient identifier** | `to[].phone_number` | `to[].phone_number` (outbound) or `to[].agent_id` + `to[].agent_name` (inbound) | | **Read receipts** | Not supported | ✅ `message.read` event | | **Suggestion responses** | Not applicable | ✅ `body.suggestion_response` | | **Location sharing** | Not supported | ✅ `body.location` | | **JSON schema** | Telnyx messaging schema | Snake-case schema based on [Google RCS API](https://developers.google.com/business-communications/rcs-business-messaging/guides/build/messages/receive) | | **Webhook URL source** | Messaging profile or per-request | RCS Agent config (inbound) + messaging profile (outbound status) | For SMS/MMS webhook handling, see [Receiving Webhooks for Messaging](/docs/messaging/messages/receiving-webhooks). --- ## Webhook URL configuration RCS webhook routing differs from SMS/MMS: | Event Type | Webhook 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 If `webhook_url` and `webhook_failover_url` are provided in the send request body, those are used. If the messaging profile has webhook URLs configured, those are used. If neither is configured, no webhook is delivered. Events are still logged in [Message Detail Records](/docs/messaging/messages/message-detail-records). --- ## Webhook event types | Event | Description | Direction | |-------|-------------|-----------| | `message.sent` | Message sent to the upstream RCS provider | Outbound | | `message.finalized` | Delivery confirmed or failed by carrier | Outbound | | `message.read` | Recipient read the message on their device | Outbound | | `message.received` | Inbound message received by your RCS Agent | Inbound | --- ## Delivery status webhooks When you send an RCS message, Telnyx notifies you as the message progresses through each status. ### Delivery status flow ```mermaid flowchart LR A[queued] --> B[sending] B --> C[sent] C --> D[delivered] D --> E[read] B --> F[sending_failed] C --> G[delivery_failed] C --> H[delivery_unconfirmed] ``` ### Status reference | Status | Description | |--------|-------------| | `queued` | Message queued on Telnyx's side | | `sending` | Being sent to the upstream RCS provider | | `sent` | Sent to the upstream provider | | `delivered` | Carrier confirmed delivery to the device | | `read` | Message read on the recipient's device | | `sending_failed` | Failed to send to the upstream provider | | `delivery_failed` | Carrier could not deliver to the recipient | | `delivery_unconfirmed` | No delivery confirmation received | ### Example: Delivery receipt payload ```json { "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 ```json { "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: ```json { "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" } } ``` ```json { "data": { "event_type": "message.received", "payload": { "body": { "user_file": { "payload": { "file_name": "photo.jpg", "file_size_bytes": 179099, "file_uri": "https://rcs-inbound.us-central-1.telnyxcloudstorage.com/rcs/.../photo.jpg", "mime_type": "image/jpeg" }, "thumbnail": { "file_name": "photo_thumb.jpg", "file_size_bytes": 12074, "file_uri": "https://rcs-inbound.us-central-1.telnyxcloudstorage.com/rcs/.../photo_thumb.jpg", "mime_type": "image/jpeg" } } }, "direction": "inbound", "type": "RCS" } } } ``` Unlike MMS where media URLs are in `payload.media[]`, RCS file attachments are nested under `payload.body.user_file` with both a full-resolution `payload` and a `thumbnail`. ```json { "data": { "event_type": "message.received", "payload": { "body": { "location": { "latitude": 38.24961321640261, "longitude": -85.78378468751907 } }, "direction": "inbound", "type": "RCS" } } } ``` When a user taps a [suggested action or reply](/docs/messaging/messages/rcs-capabilities) that you included in a previous message: ```json { "data": { "event_type": "message.received", "payload": { "body": { "suggestion_response": { "postback_data": "action_visit_store", "text": "Explore the online store" } }, "direction": "inbound", "type": "RCS" } } } ``` Use `postback_data` for programmatic routing and `text` for the user-visible label. --- ## SDK webhook handling examples Handle RCS webhooks in your application with proper event routing, signature verification, and message type detection. ```python Python 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) ``` ```javascript Node const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhooks/rcs', (req, res) => { const event = req.body; const eventType = event.data.event_type; const payload = event.data.payload; switch (eventType) { case 'message.received': handleInbound(payload); break; case 'message.sent': console.log(`Message ${payload.id} sent`); break; case 'message.finalized': handleDelivery(payload); break; case 'message.read': handleReadReceipt(payload); break; } res.status(200).json({ status: 'ok' }); }); function handleInbound(payload) { const body = payload.body; const sender = payload.from.phone_number; if (body.text) { console.log(`Text from ${sender}: ${body.text}`); } else if (body.user_file) { const file = body.user_file.payload; console.log(`File from ${sender}: ${file.file_name} (${file.mime_type})`); } else if (body.location) { console.log(`Location from ${sender}: ${body.location.latitude}, ${body.location.longitude}`); } else if (body.suggestion_response) { const resp = body.suggestion_response; console.log(`Suggestion from ${sender}: ${resp.text} (postback: ${resp.postback_data})`); } } function handleDelivery(payload) { const messageId = payload.id; for (const recipient of payload.to || []) { console.log(`Message ${messageId} to ${recipient.phone_number}: ${recipient.status}`); if (recipient.status === 'delivery_failed') { console.log(`Delivery failed for ${recipient.phone_number}, triggering SMS fallback`); } } } function handleReadReceipt(payload) { const messageId = payload.id; for (const recipient of payload.to || []) { console.log(`Message ${messageId} read by ${recipient.phone_number}`); // Update message status in your database } } app.listen(5000, () => console.log('RCS webhook server running on port 5000')); ``` ```ruby Ruby require "sinatra" require "json" set :port, 5000 post "/webhooks/rcs" do event = JSON.parse(request.body.read) event_type = event.dig("data", "event_type") payload = event.dig("data", "payload") case event_type when "message.received" handle_inbound(payload) when "message.sent" puts "Message #{payload['id']} sent" when "message.finalized" handle_delivery(payload) when "message.read" handle_read_receipt(payload) end { status: "ok" }.to_json end def handle_inbound(payload) body = payload["body"] sender = payload.dig("from", "phone_number") if body["text"] puts "Text from #{sender}: #{body['text']}" elsif body["user_file"] file = body.dig("user_file", "payload") puts "File from #{sender}: #{file['file_name']} (#{file['mime_type']})" elsif body["location"] loc = body["location"] puts "Location from #{sender}: #{loc['latitude']}, #{loc['longitude']}" elsif body["suggestion_response"] resp = body["suggestion_response"] puts "Suggestion from #{sender}: #{resp['text']} (postback: #{resp['postback_data']})" end end def handle_delivery(payload) payload.fetch("to", []).each do |recipient| puts "Message #{payload['id']} to #{recipient['phone_number']}: #{recipient['status']}" end end def handle_read_receipt(payload) payload.fetch("to", []).each do |recipient| puts "Message #{payload['id']} read by #{recipient['phone_number']}" end end ``` ```java Java import com.sun.net.httpserver.*; import java.io.*; import java.net.InetSocketAddress; import org.json.*; public class RcsWebhookServer { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress(5000), 0); server.createContext("/webhooks/rcs", exchange -> { if (!"POST".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(405, -1); return; } String body = new String(exchange.getRequestBody().readAllBytes()); JSONObject event = new JSONObject(body); String eventType = event.getJSONObject("data").getString("event_type"); JSONObject payload = event.getJSONObject("data").getJSONObject("payload"); switch (eventType) { case "message.received" -> handleInbound(payload); case "message.finalized" -> handleDelivery(payload); case "message.read" -> handleReadReceipt(payload); default -> System.out.println("Event: " + eventType); } String response = "{\"status\":\"ok\"}"; exchange.sendResponseHeaders(200, response.length()); exchange.getResponseBody().write(response.getBytes()); exchange.close(); }); server.start(); System.out.println("RCS webhook server running on port 5000"); } static void handleInbound(JSONObject payload) { JSONObject body = payload.getJSONObject("body"); String sender = payload.getJSONObject("from").getString("phone_number"); if (body.has("text")) { System.out.printf("Text from %s: %s%n", sender, body.getString("text")); } else if (body.has("suggestion_response")) { JSONObject resp = body.getJSONObject("suggestion_response"); System.out.printf("Suggestion from %s: %s%n", sender, resp.getString("text")); } } static void handleDelivery(JSONObject payload) { String msgId = payload.getString("id"); JSONArray to = payload.getJSONArray("to"); for (int i = 0; i < to.length(); i++) { JSONObject r = to.getJSONObject(i); System.out.printf("Message %s to %s: %s%n", msgId, r.getString("phone_number"), r.getString("status")); } } static void handleReadReceipt(JSONObject payload) { String msgId = payload.getString("id"); JSONArray to = payload.getJSONArray("to"); for (int i = 0; i < to.length(); i++) { System.out.printf("Message %s read by %s%n", msgId, to.getJSONObject(i).getString("phone_number")); } } } ``` ```csharp .NET using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using System.Text.Json; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/webhooks/rcs", async (HttpContext context) => { var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); var doc = JsonDocument.Parse(body); var data = doc.RootElement.GetProperty("data"); var eventType = data.GetProperty("event_type").GetString(); var payload = data.GetProperty("payload"); switch (eventType) { case "message.received": HandleInbound(payload); break; case "message.finalized": HandleDelivery(payload); break; case "message.read": HandleReadReceipt(payload); break; } return Results.Json(new { status = "ok" }); }); void HandleInbound(JsonElement payload) { var body = payload.GetProperty("body"); var sender = payload.GetProperty("from").GetProperty("phone_number").GetString(); if (body.TryGetProperty("text", out var text)) Console.WriteLine($"Text from {sender}: {text}"); else if (body.TryGetProperty("suggestion_response", out var suggestion)) Console.WriteLine($"Suggestion from {sender}: {suggestion.GetProperty("text")}"); else if (body.TryGetProperty("location", out var location)) Console.WriteLine($"Location from {sender}: {location.GetProperty("latitude")}, {location.GetProperty("longitude")}"); } void HandleDelivery(JsonElement payload) { var msgId = payload.GetProperty("id").GetString(); foreach (var r in payload.GetProperty("to").EnumerateArray()) Console.WriteLine($"Message {msgId} to {r.GetProperty("phone_number")}: {r.GetProperty("status")}"); } void HandleReadReceipt(JsonElement payload) { var msgId = payload.GetProperty("id").GetString(); foreach (var r in payload.GetProperty("to").EnumerateArray()) Console.WriteLine($"Message {msgId} read by {r.GetProperty("phone_number")}"); } app.Run("http://0.0.0.0:5000"); ``` ```php PHP 'ok']); function handleInbound(array $payload): void { $body = $payload['body']; $sender = $payload['from']['phone_number']; if (isset($body['text'])) { echo "Text from {$sender}: {$body['text']}\n"; } elseif (isset($body['user_file'])) { $file = $body['user_file']['payload']; echo "File from {$sender}: {$file['file_name']} ({$file['mime_type']})\n"; } elseif (isset($body['location'])) { $loc = $body['location']; echo "Location from {$sender}: {$loc['latitude']}, {$loc['longitude']}\n"; } elseif (isset($body['suggestion_response'])) { $resp = $body['suggestion_response']; echo "Suggestion from {$sender}: {$resp['text']} (postback: {$resp['postback_data']})\n"; } } function handleDelivery(array $payload): void { foreach ($payload['to'] ?? [] as $recipient) { echo "Message {$payload['id']} to {$recipient['phone_number']}: {$recipient['status']}\n"; } } function handleReadReceipt(array $payload): void { foreach ($payload['to'] ?? [] as $recipient) { echo "Message {$payload['id']} read by {$recipient['phone_number']}\n"; } } ``` ```go Go package main import ( "encoding/json" "fmt" "io" "net/http" ) type WebhookEvent struct { Data struct { EventType string `json:"event_type"` Payload struct { ID string `json:"id"` Body struct { Text string `json:"text,omitempty"` Location map[string]float64 `json:"location,omitempty"` SuggestionResponse map[string]string `json:"suggestion_response,omitempty"` UserFile map[string]interface{} `json:"user_file,omitempty"` } `json:"body"` From struct { PhoneNumber string `json:"phone_number"` AgentID string `json:"agent_id"` } `json:"from"` To []struct { PhoneNumber string `json:"phone_number"` Status string `json:"status"` } `json:"to"` } `json:"payload"` } `json:"data"` } func main() { http.HandleFunc("/webhooks/rcs", func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var event WebhookEvent json.Unmarshal(body, &event) switch event.Data.EventType { case "message.received": handleInbound(event) case "message.finalized": handleDelivery(event) case "message.read": handleReadReceipt(event) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) fmt.Fprint(w, `{"status":"ok"}`) }) fmt.Println("RCS webhook server running on port 5000") http.ListenAndServe(":5000", nil) } func handleInbound(event WebhookEvent) { p := event.Data.Payload sender := p.From.PhoneNumber if p.Body.Text != "" { fmt.Printf("Text from %s: %s\n", sender, p.Body.Text) } else if p.Body.SuggestionResponse != nil { fmt.Printf("Suggestion from %s: %s (postback: %s)\n", sender, p.Body.SuggestionResponse["text"], p.Body.SuggestionResponse["postback_data"]) } else if p.Body.Location != nil { fmt.Printf("Location from %s: %f, %f\n", sender, p.Body.Location["latitude"], p.Body.Location["longitude"]) } } func handleDelivery(event WebhookEvent) { for _, r := range event.Data.Payload.To { fmt.Printf("Message %s to %s: %s\n", event.Data.Payload.ID, r.PhoneNumber, r.Status) } } func handleReadReceipt(event WebhookEvent) { for _, r := range event.Data.Payload.To { fmt.Printf("Message %s read by %s\n", event.Data.Payload.ID, r.PhoneNumber) } } ``` --- ## Building a unified SMS + RCS webhook handler If your application handles both SMS/MMS and RCS, you need to normalize the different payload structures: ```python Python 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"), } ``` ```javascript Node function normalizeMessage(event) { const payload = event.data.payload; const msgType = payload.type || 'SMS'; if (msgType === 'RCS') { const body = payload.body || {}; const media = []; if (body.user_file) { const file = body.user_file.payload; media.push({ url: file.file_uri, contentType: file.mime_type, size: file.file_size_bytes, }); } return { id: payload.id, type: msgType, sender: payload.from?.phone_number || '', text: body.text || '', media, direction: payload.direction, }; } // SMS/MMS return { id: payload.id, type: msgType, sender: payload.from?.phone_number || '', text: payload.text || '', media: (payload.media || []).map(m => ({ url: m.url, contentType: m.content_type, })), direction: payload.direction, }; } ``` --- ## Webhook IP allowlist If your server uses a firewall or ACL, allowlist the Telnyx webhook source subnet: | CIDR | Description | |------|-------------| | `192.76.120.192/27` | Telnyx 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](/docs/messaging/messages/receiving-webhooks#webhook-signature-verification) 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 Send rich cards, carousels, and suggested actions via RCS. Check device support and RCS feature availability. Compare with the SMS/MMS webhook handling guide. Set up your RCS Agent and start messaging. --- ## API Reference (RCS) ### RCS - [Send an RCS message](https://developers.telnyx.com/api-reference/rcs/send-an-rcs-message.md) - [List all RCS agents](https://developers.telnyx.com/api-reference/rcs/list-all-rcs-agents.md) - [Retrieve an RCS agent](https://developers.telnyx.com/api-reference/rcs/retrieve-an-rcs-agent.md) - [Modify an RCS agent](https://developers.telnyx.com/api-reference/rcs/modify-an-rcs-agent.md) - [Check RCS capabilities (batch)](https://developers.telnyx.com/api-reference/rcs/check-rcs-capabilities-batch.md) - [Check RCS capabilities](https://developers.telnyx.com/api-reference/rcs/check-rcs-capabilities.md) - [Add RCS test number](https://developers.telnyx.com/api-reference/rcs/add-rcs-test-number.md): Adds a test phone number to an RCS agent for testing purposes. - [Generate RCS deeplink](https://developers.telnyx.com/api-reference/rcs/generate-rcs-deeplink.md): Generate a deeplink URL that can be used to start an RCS conversation with a specific agent.