{"id":2958,"date":"2026-09-11T09:25:52","date_gmt":"2026-09-11T03:55:52","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=2958"},"modified":"2026-09-11T09:25:54","modified_gmt":"2026-09-11T03:55:54","slug":"rcs-messaging-api-reference-migration-from-sms","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/rcs-messaging-api-reference-migration-from-sms\/","title":{"rendered":"RCS Messaging API: Complete Reference and Migration Path from SMS"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A field reference for the SMSGatewayCenter RCS API written for engineers who already run SMS. Two API generations, three response envelopes, the bot and template model, the delivery report with read and click timestamps, the inbox, and how to run RCS alongside SMS with fallback rather than replacing it.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/rcs-messaging-api-reference-migration-sms-fallback.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/rcs-messaging-api-reference-migration-sms-fallback-1024x584.webp\" alt=\"Diagram style illustration of two parallel messaging pipelines, a plain grey one and a rich teal one with layered cards and button shapes, joined by an orange fallback arc.\" class=\"wp-image-2959\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/rcs-messaging-api-reference-migration-sms-fallback-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/rcs-messaging-api-reference-migration-sms-fallback-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/rcs-messaging-api-reference-migration-sms-fallback-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/rcs-messaging-api-reference-migration-sms-fallback.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">RCS and SMS are two pipelines that run side by side, joined by a fallback path. They are not one pipeline with a richer payload.<\/figcaption><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\">TABLE OF CONTENTS<\/h1>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"#the-short-answer\">The Short Answer<\/a><\/li>\n\n\n\n<li><a href=\"#tldr\">TL;DR<\/a><\/li>\n\n\n\n<li><a href=\"#what-rcs-changes\">What RCS Actually Changes, at the Protocol and Billing Level<\/a><\/li>\n\n\n\n<li><a href=\"#two-generations\">Two API Generations Inside One Product<\/a><\/li>\n\n\n\n<li><a href=\"#bots\">Bots: The Identity Layer SMS Does Not Have<\/a><\/li>\n\n\n\n<li><a href=\"#templates\">Templates: Create, List, Delete, and the Approval Clock<\/a><\/li>\n\n\n\n<li><a href=\"#send-methods\">The Three Send Methods and Why the Response Union Collapsed<\/a><\/li>\n\n\n\n<li><a href=\"#correlation-chain\">The Correlation Chain and Its Two Renames<\/a><\/li>\n\n\n\n<li><a href=\"#delivery-report\">The Delivery Report: Read and Click Are New Terminal States<\/a><\/li>\n\n\n\n<li><a href=\"#inbox\">The Inbox: Two-Way RCS Is a Polling Problem<\/a><\/li>\n\n\n\n<li><a href=\"#summary-endpoint\">The Summary Endpoint and Reconciliation<\/a><\/li>\n\n\n\n<li><a href=\"#endpoint-reference\">Endpoint Reference Table<\/a><\/li>\n\n\n\n<li><a href=\"#success-codes\">Success Codes Across the RCS Families<\/a><\/li>\n\n\n\n<li><a href=\"#rcs-vs-sms\">RCS Against SMS: A Field-Level Comparison<\/a><\/li>\n\n\n\n<li><a href=\"#fallback\">Fallback Is the Migration Path, Not Replacement<\/a><\/li>\n\n\n\n<li><a href=\"#first-send\">Your First RCS Send in Six Steps<\/a><\/li>\n\n\n\n<li><a href=\"#working-code\">Working Code: cURL, Python, PHP, Node.js<\/a><\/li>\n\n\n\n<li><a href=\"#ten-mistakes\">Ten Mistakes That Cost a Week<\/a><\/li>\n\n\n\n<li><a href=\"#decision-matrix\">A Decision Matrix for Channel Choice<\/a><\/li>\n\n\n\n<li><a href=\"#checklist\">Production Readiness Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#unspecified-behaviour\">Unspecified Behaviour and How to Code Around It<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"the-short-answer\" class=\"wp-block-heading\">The Short Answer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The RCS API on SMSGatewayCenter is a different API from the SMS API, not a channel flag on the same one. Sends go to <code>https:\/\/unify.smsgateway.center\/RCSApi\/send<\/code> rather than <code>SMSApi\/send<\/code>, they require a <code>botId<\/code> instead of a <code>senderid<\/code>, the message body is a JSON payload naming an approved <code>templateCode<\/code> rather than free text, and the format parameter is spelled <code>format<\/code> here where the SMS API spells it <code>output<\/code>. Reporting lives on a second, newer base path entirely, <code>https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/<\/code>, which uses <code>page<\/code> and <code>limit<\/code> paging, accepts an <code>apikey<\/code> header, and returns a flat envelope with no <code>response<\/code> wrapper. Three distinct response envelope shapes exist across the RCS endpoints, so a single parser will not cover them. The practical migration is not a cutover: you keep your SMS path, add an RCS path, and route between them, because RCS only reaches handsets where the recipient has RCS available and enabled, and the delivery report tells you exactly when it did not with <code>globalErrorCode<\/code> 5007 and <code>cause<\/code> &#8220;RCS Not Enabled&#8221;.<\/p>\n\n\n\n<h2 id=\"tldr\" class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Base path:<\/strong> <code>https:\/\/unify.smsgateway.center\/RCSApi\/send<\/code>, POST, <code>application\/x-www-form-urlencoded<\/code>. Documented at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-rcs-batch\/\">Send RCS Message API (Quick)<\/a>.<\/li>\n\n\n\n<li><strong>Three send methods, same endpoint:<\/strong> <code>sendMethod=quick<\/code> with <code>mobile<\/code>, <code>sendMethod=group<\/code> with <code>group<\/code>, <code>sendMethod=bulkupload<\/code> with <code>phoneHeader<\/code> and <code>customUpload<\/code> over <code>multipart\/form-data<\/code>.<\/li>\n\n\n\n<li><strong>Unlike the SMS send, all three documented RCS send responses carry the same seven keys.<\/strong> <code>status<\/code>, <code>mobile<\/code>, <code>invalidMobile<\/code>, <code>transactionId<\/code>, <code>statusCode<\/code>, <code>reason<\/code>, <code>msgId<\/code>. Parse one shape.<\/li>\n\n\n\n<li><strong><code>format=json<\/code>, not <code>output=json<\/code>, on every <code>RCSApi\/<\/code> endpoint.<\/strong> The <code>rest\/rcs\/v1\/<\/code> endpoints flip back to <code>output=json<\/code>. Getting this wrong is the most common first-hour failure.<\/li>\n\n\n\n<li><strong>You need a bot before you need anything else.<\/strong> <code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/bots<\/code> lists them. <code>botId<\/code> is an unquoted integer there and a form field everywhere else.<\/li>\n\n\n\n<li><strong>The template name is the template code.<\/strong> <code>RCSApi\/template\/create<\/code> returns <code>templateName<\/code>, <code>RCSApi\/template\/list<\/code> returns the same value as <code>name<\/code>, and you send it as <code>contentMessage.templateMessage.templateCode<\/code>.<\/li>\n\n\n\n<li><strong>Placeholders are square brackets, <code>[name]<\/code> and <code>[otp]<\/code>,<\/strong> not the DLT <code>{#var#}<\/code> syntax you use on SMS.<\/li>\n\n\n\n<li><strong>The delivery report is a REST endpoint,<\/strong> <code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/dlr<\/code>, with <code>page<\/code> and <code>limit<\/code> up to 200, not cursor paging.<\/li>\n\n\n\n<li><strong>RCS delivery rows carry <code>readTime<\/code> and <code>clickedTime<\/code>.<\/strong> SMS has no equivalent. This is the single largest reporting difference.<\/li>\n\n\n\n<li><strong><code>identifier<\/code> on send becomes <code>campaignName<\/code> on the report.<\/strong> Same value, two names, and it is the only caller-supplied correlation token on any send surface on this platform.<\/li>\n\n\n\n<li><strong>Two-way RCS is polling.<\/strong> <code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/inbox<\/code> is documented as read-only with <code>page<\/code> and <code>limit<\/code>.<\/li>\n\n\n\n<li><strong>Store <code>transactionId<\/code> and <code>uuId<\/code> as text.<\/strong> Nineteen digits exceeds what a 64-bit float represents exactly, and several JSON parsers will silently mangle them.<\/li>\n\n\n\n<li><strong>Run both channels.<\/strong> <a href=\"https:\/\/www.smsgatewaycenter.com\/multi-channel-fallback\/\">Multi-Channel Fallback<\/a> is the mechanism, and its behaviour is described in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-are-trigger-error-codes-in-multi-channel-fallback\/\">What are trigger error codes in multi-channel fallback<\/a>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"what-rcs-changes\" class=\"wp-block-heading\">What RCS Actually Changes, at the Protocol and Billing Level<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you already operate an SMS integration, the useful way to understand RCS is by what it removes and what it adds to the contract you already know.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SMS is a store-and-forward transport with a hard 160 GSM-7 character unit, a sender header, and a binary-ish delivery outcome. Everything you build on top of it, segment accounting, template approval, delivery reconciliation, follows from those three facts. RCS is an IP-based messaging service standardised by the GSMA under the <a href=\"https:\/\/www.gsma.com\/solutions-and-impact\/technologies\/networks\/rcs\/universal-profile\/\" target=\"_blank\" rel=\"noopener nofollow\">Universal Profile<\/a>, delivered over data rather than the signalling channel, rendered by the handset&#8217;s native messaging app, and identified by a verified business agent rather than an alphanumeric header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Four consequences matter to your code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. The message is structured, not a string.<\/strong> On the SMS API you pass <code>msg<\/code> as text and the platform counts segments for you. On RCS you pass <code>msg<\/code> as a JSON document. In the documented form it looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"contentMessage\": {\n    \"templateMessage\": {\n      \"templateCode\": \"WELCOME_TEMPLATE\"\n    }\n  },\n  \"ttl\": \"86400s\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">There is no character count to compute, no GSM-7 versus Unicode branch, and no concatenation arithmetic. Everything you built around <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-length-calculation-credits-counted\/\">SMS length and credit counting<\/a> has no analogue on this path. That is a subtraction, and it is a large one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Identity moves from a six-character header to a bot.<\/strong> Where SMS sends carry <code>senderid<\/code>, RCS sends carry <code>botId<\/code>. The bot is a registered agent with a name and a logo, and the handset renders it as a verified brand rather than as a string of capitals. Practically, this means your sender-identity provisioning work moves from the sender ID lifecycle into bot provisioning, and a bot you do not own cannot be used, since the documentation states plainly that the <code>botId<\/code> must belong to the account you authenticate as.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Delivery has more terminal states.<\/strong> An SMS is submitted, then delivered or failed. An RCS message is submitted, then delivered, then possibly read, then possibly clicked. The delivery report row exposes all four as separate timestamps. Your outbound message table, if you built it along the lines described in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">The Outbound Message Table<\/a>, needs two more nullable timestamp columns and needs to stop treating &#8220;delivered&#8221; as terminal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. The message can fail for a reason SMS never has.<\/strong> The documented sample delivery row on the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/rcs-delivery-report\/\">RCS Delivery Report REST API<\/a> page shows <code>globalErrorCode<\/code> 5007 with <code>cause<\/code> &#8220;RCS Not Enabled&#8221; and <code>deliveryStatus<\/code> &#8220;FAILED&#8221;. A recipient whose handset or network does not have RCS available is not a bad number and not a transient error. It is a permanent routing fact about that recipient for that moment, and it is the reason the migration section of this reference argues for fallback rather than cutover.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On billing, the documented delivery row carries <code>amount<\/code> as the quoted string <code>\"0.0000\"<\/code>. Because that sample row is a failed send, it establishes only that a failed RCS message is priced at zero, which is what you would expect and what the SMS side does too. For live per-message and per-conversation rates, read <a href=\"https:\/\/www.smsgatewaycenter.com\/rcs-pricing\/\">RCS pricing<\/a> and the wallet figure in your own portal rather than hardcoding anything, exactly as the guidance in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/bulk-sms-pricing-in-india-what-actually-drives-cost\/\">Bulk SMS Pricing in India<\/a> recommends for the SMS channel.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"two-generations\" class=\"wp-block-heading\">Two API Generations Inside One Product<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the structural fact that makes RCS integrations go wrong, and it is visible only if you read the endpoint URLs side by side.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are two families. The first uses the base path <code>https:\/\/unify.smsgateway.center\/RCSApi\/<\/code> and covers sending and template management. It behaves like the rest of the SMSGatewayCenter platform: POST with form-encoded bodies, credentials in the body, no paging.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The second uses the base path <code>https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/<\/code> and covers bots, delivery reports, the inbox, and summary. It behaves like a modern REST API: GET only, query parameters, an <code>apikey<\/code> header as an alternative to sending your password, page-and-limit paging, and a legacy alias without the <code>v1<\/code> segment that is also accepted.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-two-api-generations.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"649\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-two-api-generations-1024x649.webp\" alt=\"Comparison diagram of the two RCS API generations on SMSGatewayCenter, the RCSApi family and the rest slash rcs slash v1 family, and the three response envelope shapes they produce.\" class=\"wp-image-2961\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-two-api-generations-1024x649.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-two-api-generations-300x190.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-two-api-generations-768x486.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-two-api-generations.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">The base path is the only signal telling you which set of rules applies to a call. Nothing in the parameter names warns you.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The differences that will actually break your first build:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Dimension<\/th><th><code>\/RCSApi\/...<\/code><\/th><th><code>\/rest\/rcs\/v1\/...<\/code><\/th><\/tr><\/thead><tbody><tr><td>HTTP method<\/td><td>POST for send, create and delete; GET for template list<\/td><td>GET<\/td><\/tr><tr><td>Format parameter name<\/td><td><code>format<\/code><\/td><td><code>output<\/code><\/td><\/tr><tr><td>API key header supported<\/td><td>Not documented<\/td><td>Yes, header <code>apikey<\/code>, and <code>password<\/code> becomes optional<\/td><\/tr><tr><td><code>userid<\/code> required<\/td><td>Yes<\/td><td>Yes, even when using the <code>apikey<\/code> header<\/td><\/tr><tr><td>Body encoding<\/td><td><code>application\/x-www-form-urlencoded<\/code>, or <code>multipart\/form-data<\/code> for file upload<\/td><td>Query string<\/td><\/tr><tr><td>Paging<\/td><td>None<\/td><td><code>page<\/code> and <code>limit<\/code>, default 50, maximum 200<\/td><\/tr><tr><td>Date filtering<\/td><td>None<\/td><td><code>fromDate<\/code> and <code>toDate<\/code>, with <code>from<\/code> and <code>to<\/code> accepted as aliases<\/td><\/tr><tr><td>Response envelope<\/td><td>Two different shapes<\/td><td>One shared shape<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Write two client classes. One speaks form-encoded POST and reads <code>format<\/code>; the other speaks GET and reads <code>output<\/code>. Trying to unify them behind a single <code>request()<\/code> helper produces a function with a boolean flag that nobody remembers to set, and the failure mode is a 200 response with an unexpected body rather than a clean error.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On envelopes, there are three shapes, not two, because the two <code>RCSApi\/<\/code> families disagree with each other.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>RCSApi\/send<\/code>, <code>RCSApi\/template\/create<\/code> and <code>RCSApi\/template\/delete<\/code> return a flat object carrying <code>status<\/code>, <code>statusCode<\/code> and <code>reason<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"reason\": \"Accepted for processing.\",\n  \"templateName\": \"my_text_template\",\n  \"status\": \"success\",\n  \"statusCode\": \"200\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>RCSApi\/template\/list<\/code> returns an object with a single <code>templates<\/code> key and nothing else at all:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"templates\": &#91; { \"name\": \"Flight_Booking\", \"type\": \"carousel\", \"status\": 1 } ]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No <code>status<\/code>. No <code>statusCode<\/code>. No <code>reason<\/code>. If your HTTP client wrapper asserts on <code>payload[\"status\"] == \"success\"<\/code> before returning, template listing will throw on every successful call.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every <code>rest\/rcs\/v1\/<\/code> endpoint returns a third shape: <code>status<\/code>, a list key named after the resource, <code>totalRecords<\/code>, <code>page<\/code>, <code>limit<\/code>, <code>fromDate<\/code>, <code>toDate<\/code>, <code>statusCode<\/code> and <code>reason<\/code>. The list key is renamed per endpoint, so it is <code>botsList<\/code>, <code>dlrList<\/code>, <code>inboxList<\/code> or <code>summaryList<\/code>. Write one unwrapper for this family, parameterised on the list key, and do not reuse it on the <code>RCSApi\/<\/code> family.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A detail worth noticing in that third shape: the bots endpoint returns <code>page<\/code>, <code>limit<\/code>, <code>fromDate<\/code> and <code>toDate<\/code> in its envelope even though it documents no date filtering and its sample shows <code>fromDate<\/code> and <code>toDate<\/code> as empty strings. The envelope is shared across the family regardless of which fields are meaningful for a given resource. Read <code>totalRecords<\/code> and the named list; treat the rest of the envelope as advisory.<\/p>\n\n\n\n<h2 id=\"bots\" class=\"wp-block-heading\">Bots: The Identity Layer SMS Does Not Have<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing else works until you know your bot id.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/bots<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Required parameters are <code>userid<\/code> and <code>output=json<\/code>, plus either <code>password<\/code> or the <code>apikey<\/code> header. <code>action<\/code> defaults to <code>list<\/code> and only <code>list<\/code> is supported in v1. The documented response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"botsList\": &#91;\n    {\n      \"botId\": 3,\n      \"name\": \"YourBot\",\n      \"status\": 1,\n      \"logoUrl\": \"https:\/\/example.com\/bots\/logo.png\",\n      \"isSandbox\": 0\n    }\n  ],\n  \"totalRecords\": 1,\n  \"page\": 1,\n  \"limit\": 1,\n  \"fromDate\": \"\",\n  \"toDate\": \"\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four things to take from this row.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>botId<\/code> is an unquoted integer here. On <code>RCSApi\/send<\/code> it is a form field and therefore a string on the wire, and the delivery report describes its <code>botId<\/code> filter as a &#8220;numeric RCS bot id&#8221;. The value is small and there is no precision hazard, but if you are round-tripping the bots list into a config file and back into a send, normalise to string once at the boundary so you are not comparing <code>3<\/code> to <code>\"3\"<\/code> in a lookup table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>status<\/code> is the integer <code>1<\/code>, and the notes state that only active bots are returned. This is the second distinct meaning of a field called <code>status<\/code> in the RCS API, after the string <code>\"success\"<\/code> on the send response, and there will be a third and fourth before this reference is finished. Never write a generic <code>is_ok(payload)<\/code> helper that keys on the name <code>status<\/code> alone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>isSandbox<\/code> is <code>0<\/code> here, and the notes say <code>1<\/code> means the bot is a sandbox bot. This is the RCS counterpart to the account-level sandbox described on the <a href=\"https:\/\/www.smsgatewaycenter.com\/demo\/\">demo page<\/a>, and it is the cleanest way to build the kind of test harness argued for in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/testing-code-that-sends-messages\/\">Testing Code That Sends Messages<\/a>. Gate on it explicitly: in your configuration loader, refuse to start a production process whose configured bot has <code>isSandbox<\/code> equal to <code>1<\/code>, and refuse to start a staging process whose bot has <code>isSandbox<\/code> equal to <code>0<\/code>. That single assertion prevents the whole class of incident where a load test bills real messages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>logoUrl<\/code> is the brand asset the handset renders. It has no effect on your send path but it is worth surfacing in any internal admin screen, because a bot with a broken logo URL looks unverified to the recipient and nobody in engineering will notice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cache the bots list. It changes when someone provisions a new agent, which is a manual, human-paced event. Refresh it on deploy and on a daily schedule; do not call it before every send.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"templates\" class=\"wp-block-heading\">Templates: Create, List, Delete, and the Approval Clock<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">RCS templates are managed on the <code>RCSApi\/<\/code> generation, and the family is create, list and delete. There is no documented update endpoint, which means editing a template is a delete-and-recreate operation and your deployment tooling should treat template definitions as immutable artefacts named by version.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Creating a template<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>POST https:\/\/unify.smsgateway.center\/RCSApi\/template\/create\nContent-Type: application\/x-www-form-urlencoded<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Parameters are <code>userid<\/code>, <code>password<\/code>, <code>botId<\/code>, and <code>templateData<\/code>, where <code>templateData<\/code> is a JSON string. The documented sample:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"my_text_template\",\n  \"type\": \"text_message\",\n  \"templateState\": \"Create\",\n  \"fallbackText\": \"\",\n  \"category\": \"TRANSACTIONAL\",\n  \"textMessageContent\": \"Hello &#91;name], your OTP is &#91;otp].\",\n  \"suggestions\": &#91;]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Supported <code>type<\/code> values documented in the notes are <code>text_message<\/code>, <code>rich_card<\/code> and <code>carousel<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The response is short and it is the most important thing on this page:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"reason\": \"Accepted for processing.\",\n  \"templateName\": \"my_text_template\",\n  \"status\": \"success\",\n  \"statusCode\": \"200\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;Accepted for processing&#8221; is not &#8220;approved&#8221;. A <code>statusCode<\/code> of <code>\"200\"<\/code> here means the platform took your submission, in exactly the way a <code>200<\/code> from an SMS send means the message was accepted for submission rather than delivered. Build your template deployment as a two-phase operation: submit, then poll <code>template\/list<\/code> until the template appears with <code>status<\/code> equal to <code>1<\/code>, and only then flip the feature flag that lets production traffic reference it. Treating the create response as completion will produce sends against a template code the platform does not yet recognise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note the placeholder syntax. <code>[name]<\/code> and <code>[otp]<\/code>, square brackets, no hash marks. If you are carrying DLT variable-substitution code over from the SMS side, where the syntax is <code>{#var#}<\/code> as covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/dlt-sms-message-content-template-registration-guide\/\">DLT template registration guide<\/a>, that code will not fire on RCS content and your substitution will silently no-op, shipping a message that says &#8220;Hello [name]&#8221; to a real customer. Write a separate substitution function with its own tests, and have it assert that zero unreplaced <code>[<\/code> characters remain in the rendered output before the send call is made.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>fallbackText<\/code> appears in the create payload with an empty string in the sample. The field name is self-describing and the sensible engineering assumption is that it is the text used when rich rendering is unavailable, but the sample does not populate it and the notes do not describe its behaviour. Populate it anyway with a complete, standalone SMS-length message that makes sense on its own, because a field that might be a fallback and is empty can only ever hurt you, and a field that might be a fallback and contains a good message cannot.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Listing templates<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/unify.smsgateway.center\/RCSApi\/template\/list?userid=...&amp;password=...&amp;botId=...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>botId<\/code> is required, so templates are scoped per bot rather than per account. If you run several bots you will call this once per bot.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The response rows look like this, abbreviated:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"templates\": &#91;\n    {\n      \"data\": \"{\\\"templateModel\\\":{\\\"name\\\":\\\"Flight_Booking\\\",\\\"type\\\":\\\"carousel\\\", ... }}\",\n      \"creationTime\": 1784978480698,\n      \"botName\": \"YourBot\",\n      \"name\": \"Flight_Booking\",\n      \"updationTime\": 1784978480698,\n      \"category\": \"Promotional\",\n      \"type\": \"carousel\",\n      \"status\": 1\n    }\n  ]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three traps in this row.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>data<\/code> is double-encoded JSON.<\/strong> It is a string whose contents are a JSON document, so reading the carousel structure requires a second parse of a value your first parse already returned. In Python that is <code>json.loads(row[\"data\"])<\/code>; in PHP <code>json_decode($row['data'], true)<\/code>; in Node <code>JSON.parse(row.data)<\/code>. Wrap it in its own try-catch and treat a parse failure as a skipped row rather than a fatal error, because one malformed template should not break the listing of the others.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>category<\/code> changes case between request and response.<\/strong> You submit <code>\"TRANSACTIONAL\"<\/code> in the create payload and the list response returns <code>\"Promotional\"<\/code> in title case. Compare categories case-insensitively, and normalise on read.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The notes state it explicitly: &#8220;Use the template <code>name<\/code> as <code>templateCode<\/code> when sending RCS.&#8221;<\/strong> This closes what would otherwise be the ugliest gap in the family, because <code>template\/create<\/code> never returns a separate code and <code>RCSApi\/send<\/code> demands <code>templateCode<\/code>. One value, three field names: <code>templateName<\/code> on create, <code>name<\/code> on list, <code>templateCode<\/code> inside the send payload. Write a single adapter function that reads the canonical value out of any of the three and returns it, and use it everywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>creationTime<\/code> and <code>updationTime<\/code> are unquoted millisecond epochs. Both hold the same value in both documented sample rows, so a template that has never been edited reports an update time equal to its creation time rather than a null or a zero. That is a friendlier convention than the &#8220;0 means never&#8221; pattern used by campaign records elsewhere on the platform, but it means you cannot detect &#8220;never edited&#8221; from the timestamps alone; compare them for equality if you need that signal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>status<\/code> here is the integer <code>1<\/code>, the third distinct meaning of that field name in this API.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Deleting a template<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>POST https:\/\/unify.smsgateway.center\/RCSApi\/template\/delete<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Parameters are <code>userid<\/code>, <code>password<\/code>, <code>botId<\/code> and <code>templateName<\/code>. The success response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"reason\": \"Template deleted successfully.\",\n  \"status\": \"success\",\n  \"statusCode\": \"200\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The safe way to call this is with the exact <code>name<\/code> string you read back from <code>template\/list<\/code>, never a value you constructed or remembered. Read the list, find the row, take its <code>name<\/code> verbatim, pass that, then read the list again and confirm the row is gone. That round-trip costs one extra GET and removes any question about what the delete key is keyed on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Deletion is irreversible from the API&#8217;s point of view, and the notes confirm that deleted templates cannot be used in send requests. Before deleting, query your own outbound table for recent sends referencing that template code. If any are still awaiting a terminal delivery state, wait. The delivery report joins on <code>templateName<\/code>, and you do not want to remove the row your reporting is about to look up.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rich cards and carousels<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The documented <code>template\/list<\/code> sample shows the two structured types in full, and they are worth reading because they tell you what the create payload can hold.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A <code>rich_card<\/code> template carries <code>orientation<\/code> (the sample shows <code>\"VERTICAL\"<\/code>), <code>height<\/code> (the sample shows <code>\"MEDIUM_HEIGHT\"<\/code>), and a <code>standAlone<\/code> object containing <code>cardTitle<\/code>, <code>cardDescription<\/code>, <code>mediaUrl<\/code> and a <code>suggestions<\/code> array.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A <code>carousel<\/code> template carries <code>height<\/code> (the sample shows <code>\"SHORT_HEIGHT\"<\/code>), <code>width<\/code> (the sample shows <code>\"SMALL_WIDTH\"<\/code>), and a <code>carouselList<\/code> array whose entries each carry <code>cardTitle<\/code>, <code>cardDescription<\/code>, <code>mediaUrl<\/code> and their own <code>suggestions<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Suggestions in both cases look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"suggestionType\": \"reply\",\n  \"displayText\": \"View QR\",\n  \"postback\": \"View QR_&#91;message_id]\",\n  \"preference\": 1\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>postback<\/code> value is what comes back to you when the recipient taps the button, and the sample shows a <code>[message_id]<\/code> placeholder appended to a human-readable label. Design your postbacks as machine-parseable tokens with a stable prefix, because your inbox reader will be pattern-matching on them and a postback of &#8220;Learn More&#8221; tells you nothing about which send it belongs to. A shape like <code>ORDERCONF_TRACK_[message_id]<\/code> gives you an action, a context and a correlation hook in one string.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Both structured types carry <code>createdViaApi: true<\/code> in the sample, so templates created through the portal and templates created through the API are distinguishable in the listing. If you are moving an existing portal-managed template estate under version control, that flag is how you audit the migration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>mediaUrl<\/code> points at a publicly reachable asset. Host it somewhere with a stable URL and a long cache lifetime, and never at a signed URL that expires, because the template outlives the send.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"send-methods\" class=\"wp-block-heading\">The Three Send Methods and Why the Response Union Collapsed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All three send methods POST to the same endpoint.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST https:\/\/unify.smsgateway.center\/RCSApi\/send<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Common required parameters across all three: <code>userid<\/code>, <code>password<\/code>, <code>sendMethod<\/code>, <code>msgType<\/code> (use <code>text<\/code> for template-based content), <code>format<\/code> (<code>json<\/code>), <code>botId<\/code>, and <code>msg<\/code>. Optional on all three: <code>identifier<\/code>, a campaign identifier for your records.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The per-method parameters:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><code>sendMethod<\/code><\/th><th>Additional required parameters<\/th><th>Encoding<\/th><\/tr><\/thead><tbody><tr><td><code>quick<\/code><\/td><td><code>mobile<\/code>, one or more numbers with country code, comma-separated<\/td><td><code>application\/x-www-form-urlencoded<\/code><\/td><\/tr><tr><td><code>group<\/code><\/td><td><code>group<\/code>, a saved group name<\/td><td><code>application\/x-www-form-urlencoded<\/code><\/td><\/tr><tr><td><code>bulkupload<\/code><\/td><td><code>phoneHeader<\/code> naming the column that holds numbers, and <code>customUpload=true<\/code><\/td><td><code>multipart\/form-data<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>group<\/code> parameter is documented as a saved group name, which connects the RCS send path directly to the contact and group model reachable through <code>SMSApi\/group\/read<\/code> and <code>SMSApi\/contact\/read<\/code>. Your audience data does not need to be duplicated for RCS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now the part that will surprise anyone who has parsed the SMS send response. On the SMS API, <code>SMSApi\/send<\/code> returns a union of four different key sets discriminated by a <code>sendMethod<\/code> you have to remember yourself, because the response does not echo it: the quick shape carries <code>mobile<\/code> and <code>invalidMobile<\/code>, the group shape carries <code>group<\/code> instead and drops both, and the file shape drops all three.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The RCS API does not do this. All three documented RCS send pages publish the identical response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"mobile\": \"919999999999\",\n  \"invalidMobile\": \"\",\n  \"transactionId\": \"3983571817594375469\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\",\n  \"msgId\": \"\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Seven keys, the same seven, on quick, on group and on bulkupload. That is a genuinely better contract and it means one response model covers the whole send surface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two caveats on how far to trust it. The <code>mobile<\/code> field is populated with a single number even on the group and file samples, which is not information a group send can meaningfully return; and <code>msgId<\/code> is an empty string in all three. The defensive read, which is correct whichever way the live behaviour goes, is: bind <code>status<\/code>, <code>statusCode<\/code>, <code>reason<\/code> and <code>transactionId<\/code> as required fields, and bind <code>mobile<\/code>, <code>invalidMobile<\/code> and <code>msgId<\/code> as optional fields that may be empty, absent, or hold a single value on a multi-recipient send. Never derive your recipient list from the response. You know who you sent to; you put them in the request. Persist that list yourself at submission time, as <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">The Outbound Message Table<\/a> argues, and use <code>transactionId<\/code> to join the delivery report back onto it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>invalidMobile<\/code> deserves a line of its own. When populated it is your only signal that some recipients in a batch were rejected before submission. Log it at warning level with the full request recipient count alongside, so that a batch of 500 that returns three invalid numbers is visible in your metrics rather than quietly becoming a 497-message send that reconciles short.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On <code>ttl<\/code>: the documented sample uses <code>\"86400s\"<\/code>, a string with a trailing <code>s<\/code>, inside the <code>msg<\/code> payload rather than as a form parameter. Set it deliberately. An OTP with a 24-hour time to live is wrong; set it to the OTP validity window and let the network stop trying afterwards. A promotional card with a 60-second time to live is also wrong. This parameter is one of the few real levers RCS gives you over delivery behaviour and most integrations leave it at whatever the first sample showed.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"correlation-chain\" class=\"wp-block-heading\">The Correlation Chain and Its Two Renames<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Five identifiers thread an RCS send, and two of them change name in transit. This is the single richest source of quiet bugs in the family, because a rename does not throw; it just returns nothing and your join comes back empty.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-identifier-chain.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-identifier-chain-1024x683.webp\" alt=\"Flow diagram tracing five RCS identifiers from the bots endpoint through send to the delivery report, highlighting the two points where a field is renamed, the template name to template code hop and the identifier to campaign name hop.\" class=\"wp-image-2960\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-identifier-chain-1024x683.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-identifier-chain-300x200.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-identifier-chain-768x512.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-rcs-identifier-chain.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Both orange boxes mark a rename. Neither produces an error when you get it wrong, which is why they cost time.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>botId<\/code> survives intact.<\/strong> It is <code>botId<\/code> in the bots list, <code>botId<\/code> in the send request, <code>botId<\/code> in the delivery report filter, and <code>botId<\/code> in the inbox and summary filters. No adapter needed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The template identifier renames once.<\/strong> Created as <code>templateName<\/code>, listed as <code>name<\/code>, sent as <code>templateCode<\/code>, and filtered on the delivery report as <code>templateName<\/code> again. Four appearances, three names.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The correlation token renames once.<\/strong> You send <code>identifier<\/code>. The delivery report filters on <code>campaignName<\/code> and returns a field called <code>campaignName<\/code>. There is no endpoint documented anywhere that calls it <code>identifier<\/code> after the send.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That second rename is worth dwelling on, because <code>identifier<\/code> is unusual and valuable. Every other send surface on this platform gives you no way to attach your own reference to a send. The SMS API has no equivalent parameter. The OTP endpoints return <code>transactionId<\/code> as the literal string <code>\"0\"<\/code>, so they offer no correlation at all. RCS is the one place you can stamp a send with a value your own system chose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use it, and use it structurally. Put a value in <code>identifier<\/code> that encodes what the send was and where it came from, something like <code>svc-orders.shipment-confirm.v3.2026-09-11<\/code>, and now the delivery report can be filtered by service, by message purpose and by template version without a join against your own database. That is a materially better operational position than the SMS side, where the same question requires you to have persisted every <code>transactionId<\/code> yourself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two constraints to design within. The documented sample row returns <code>campaignName<\/code> as an empty string, so do not assume every row will carry the value back. And the filter is documented as filtering by &#8220;campaign name&#8221;, with no statement about whether matching is exact or partial. Keep your identifiers free of characters that behave specially in a substring match, keep them unique enough that a partial match would still be correct, and keep <code>uuId<\/code> as the real primary key for reconciliation. <code>identifier<\/code> is a fast index, not the source of truth.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>uuId<\/code> and <code>transactionId<\/code> are the same concept under two names.<\/strong> The send returns <code>transactionId<\/code>. The delivery report row carries <code>uuId<\/code> and accepts <code>transactionId<\/code> as a documented request alias for the <code>uuId<\/code> filter. Both sample values are nineteen digits. Nineteen digits is above the range a double-precision float represents exactly, which means a JavaScript <code>JSON.parse<\/code> will round it and hand you a different number, silently. Store it as text in your database, keep it as a string in every language, and if you are on Node use a JSON parser configured to preserve large integers as strings, or read the field out of the raw body before parsing. This is the same hazard documented for SMS transaction identifiers in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-nodejs-integration-tutorial\/\">Node.js integration tutorial<\/a>, and it bites exactly as hard on RCS.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"delivery-report\" class=\"wp-block-heading\">The Delivery Report: Read and Click Are New Terminal States<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/dlr<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The legacy alias <code>\/rest\/rcs\/dlr<\/code> is documented as also supported. Prefer the versioned path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Required: <code>userid<\/code>, <code>output=json<\/code>, and either <code>password<\/code> or the <code>apikey<\/code> header. Optional filters: <code>action<\/code> (defaults to <code>list<\/code>, only <code>list<\/code> supported in v1), <code>fromDate<\/code> and <code>toDate<\/code> (aliases <code>from<\/code> and <code>to<\/code>, accepting <code>YYYY-MM-DD<\/code> or full date-time), <code>mobileNo<\/code>, <code>uuId<\/code> (alias <code>transactionId<\/code>), <code>botId<\/code>, <code>campaignName<\/code>, <code>templateName<\/code>, <code>globalErrorCode<\/code> (alias <code>status<\/code>), <code>globalErrorCodes<\/code> for a comma-separated set, <code>page<\/code>, and <code>limit<\/code> with a default of 50 and a documented maximum of 200.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note that <code>status<\/code> is a documented request alias for <code>globalErrorCode<\/code>. That is the fourth distinct meaning of the token <code>status<\/code> in this API, and this one is the worst, because a developer who passes <code>status=success<\/code> intending to filter for successful messages is passing a non-numeric value to a numeric error-code filter. Always spell it <code>globalErrorCode<\/code> in your code. The alias exists; using it is a trap you can simply decline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented row:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"uuId\": \"6226901107781419438\",\n  \"botName\": \"YourBot\",\n  \"mobileNo\": \"919876543210\",\n  \"msgId\": \"Zr6dILs7iZt1pvO\",\n  \"message\": \"{\\\"contentMessage\\\":{\\\"templateMessage\\\":{\\\"templateCode\\\":\\\"WELCOME_TEMPLATE\\\"}},\\\"ttl\\\":\\\"86400s\\\"}\",\n  \"amount\": \"0.0000\",\n  \"msgTypeLabel\": \"RICH\",\n  \"directionLabel\": \"A2P\",\n  \"globalErrorCode\": 5007,\n  \"deliveryStatus\": \"FAILED\",\n  \"cause\": \"RCS Not Enabled\",\n  \"channelLabel\": \"API\",\n  \"campaignName\": \"\",\n  \"submitTime\": 1788421063350,\n  \"submitTimeFormatted\": \"2026-09-03 13:07:43\",\n  \"deliveryTime\": 1788421066059,\n  \"deliveryTimeFormatted\": \"2026-09-03 13:07:46\",\n  \"readTime\": 0,\n  \"readTimeFormatted\": \"\",\n  \"clickedTime\": 0,\n  \"clickedTimeFormatted\": \"\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Read this row by type, not just by name, because the types are not consistent with the SMS delivery row and reusing an SMS parser here will produce wrong numbers rather than errors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>mobileNo<\/code> is a <strong>quoted string<\/strong> here. On the SMS delivery row it is an unquoted number. This is an improvement and it removes a real precision hazard, but it means the two rows cannot share a deserialiser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>amount<\/code> is a <strong>quoted string<\/strong>, <code>\"0.0000\"<\/code>, with four decimal places. On the SMS delivery row <code>amount<\/code> is an unquoted float. Parse to a decimal type, not a float, and never to an integer. Four decimal places implies sub-paisa pricing granularity, so a currency column with two decimals will lose money over a large volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>globalErrorCode<\/code> is an <strong>unquoted integer<\/strong>. This is your machine-readable delivery contract. <code>cause<\/code> is prose intended for humans. Branch on the number, log the prose, never parse the prose. The full numeric catalogue is available and free to read from the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/get-delivery-error-code-list\/\">Delivery Error Codes list<\/a>, and diffing it nightly is one of the cheapest contract tests you can run against this platform.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>deliveryStatus<\/code> is the field carrying <code>\"FAILED\"<\/code>. Note that it is <strong>not<\/strong> called <code>status<\/code>. The delivery outcome on the SMS row is called <code>status<\/code>; here it is <code>deliveryStatus<\/code>, and <code>status<\/code> at the envelope level means whether the API call itself succeeded. Two fields, one concept each, and the names have been swapped relative to the SMS API. This is the highest-value single line in this reference for anyone porting SMS reporting code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>msgTypeLabel<\/code> is <code>\"RICH\"<\/code> and <code>directionLabel<\/code> is <code>\"A2P\"<\/code>. The <code>Label<\/code> suffix on both, and on <code>channelLabel<\/code>, is a strong hint that unlabelled numeric twins exist somewhere in the platform, exactly as the summary endpoint publishes both <code>direction<\/code> as an integer and <code>directionLabel<\/code> as a string. Store the label. If a numeric twin appears on this row in future it will be additive, and a label is the more stable thing to have persisted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>message<\/code> is <strong>double-encoded JSON<\/strong>, the same trap as <code>data<\/code> on the template list. It is a string containing the exact <code>msg<\/code> payload you submitted. Two consequences. First, parsing it takes a second pass. Second, and more usefully, it means the delivery report is self-describing: you can recover what was actually sent from the report row alone, without joining to your own send log. That is genuinely valuable for incident forensics and it is not something the SMS delivery report gives you.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The four timestamps are the reason to be here.<\/strong> <code>submitTime<\/code>, <code>deliveryTime<\/code>, <code>readTime<\/code> and <code>clickedTime<\/code> are unquoted millisecond epochs, each with a preformatted string twin whose name ends in <code>Formatted<\/code>. Zero means the event did not occur; the corresponding <code>Formatted<\/code> field is an empty string.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Treat them as a monotonic funnel and store all four as nullable columns, mapping zero to null on ingest. That gives you, in one table, submitted-to-delivered latency, delivered-to-read latency, and read-to-clicked conversion, per message, without any instrumentation of your own. No SMS integration on any platform can produce the second and third of those.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two cautions on the arithmetic. In the documented sample, <code>deliveryTime<\/code> is populated with a real timestamp on a row whose <code>deliveryStatus<\/code> is <code>FAILED<\/code>, which means the field marks the terminal state, whatever that state was, rather than a successful delivery specifically. Compute delivery latency only on rows whose <code>globalErrorCode<\/code> indicates success, and clamp every interval at zero, because the SMS side of this platform has a documented sample where the delivery timestamp precedes the submit timestamp on the same row and there is no reason to assume the RCS clocks are better synchronised. A negative latency in a dashboard is a bug report from a customer; a clamped zero is a rounding artefact nobody notices.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>Formatted<\/code> twins carry no timezone. Ingest the epoch, not the string, and render in your own timezone at display time. Keep the string only for support tickets, where matching what the portal shows a customer is worth more than correctness.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Paging is page-and-limit, not cursors.<\/strong> Set <code>limit=200<\/code>, start at <code>page=1<\/code>, and stop when the number of rows returned is less than <code>limit<\/code> or when <code>page<\/code> multiplied by <code>limit<\/code> reaches or exceeds <code>totalRecords<\/code>. Both conditions, not one, because <code>totalRecords<\/code> on a live feed can move while you are paging.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That mutability is the real weakness of offset paging here. If new rows land while you are on page 3, rows shift and you can skip or duplicate. Two defences, both cheap. First, always bound your query with an explicit <code>fromDate<\/code> and <code>toDate<\/code> over a window that has closed, so the underlying set is stable while you read it. Second, make your ingest idempotent on <code>uuId<\/code> plus <code>mobileNo<\/code>, with an upsert rather than an insert, so a duplicated row is harmless and a re-run of yesterday&#8217;s window is free. Ingest the same way you would ingest SMS delivery data, appending facts rather than mutating a status column, which is the pattern argued for at length in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/observability-for-messaging-pipelines\/\">Observability for Messaging Pipelines<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented error response is worth handling by name:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"error\",\n  \"dlrList\": &#91;],\n  \"statusCode\": \"403\",\n  \"reason\": \"RCS API access is not enabled for this account.\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An empty <code>dlrList<\/code> with <code>statusCode<\/code> <code>\"403\"<\/code> is an entitlement problem, not an empty day. Alert on it. A poller that treats 403 as &#8220;no messages yesterday&#8221; will report perfect silence for as long as the entitlement stays off.<\/p>\n\n\n\n<h2 id=\"inbox\" class=\"wp-block-heading\">The Inbox: Two-Way RCS Is a Polling Problem<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/inbox<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Legacy alias <code>\/rest\/rcs\/inbox<\/code>. Required: <code>userid<\/code>, <code>output=json<\/code>, and either <code>password<\/code> or the <code>apikey<\/code> header. Optional: <code>action<\/code>, <code>fromDate<\/code>, <code>toDate<\/code>, <code>mobileNo<\/code> (documented as five to twenty digits with country code), <code>botId<\/code>, <code>page<\/code>, <code>limit<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented row:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"incomingId\": \"451\",\n  \"botId\": 3,\n  \"mobileNo\": \"919876543210\",\n  \"msgId\": \"abc-123\",\n  \"type\": \"text\",\n  \"typeLabel\": \"FREE TEXT\",\n  \"message\": \"Hi, is my order shipped?\",\n  \"receivedAt\": 1752480000000,\n  \"receivedAtFormatted\": \"2026-07-14 10:00:00\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>incomingId<\/code> is a quoted string while <code>botId<\/code> on the same row is an unquoted integer. <code>message<\/code> here is plain text, not double-encoded JSON, unlike the delivery row. <code>receivedAt<\/code> is an unquoted millisecond epoch with a formatted twin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>typeLabel<\/code> reads <code>\"FREE TEXT\"<\/code>, which strongly implies a second inbound type exists for suggestion taps, since suggestions carry a <code>postback<\/code> value and that value has to arrive somewhere. Design your inbox handler as a dispatch on <code>type<\/code> with an explicit default branch that logs the unrecognised type and stores the row unprocessed rather than dropping it. When suggestion replies start appearing you will have a record of them from day one instead of discovering the type after a month of silent discards.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The page states the endpoint is read-only and that to reply you use the send API. That is the whole two-way loop: poll inbox, match on <code>mobileNo<\/code> and <code>botId<\/code>, send a reply through <code>RCSApi\/send<\/code>. There is no documented inbound webhook for RCS, so build the poller and build it properly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A poller design that holds up in production, in four parts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Poll on a fixed short interval with an overlapping window.<\/strong> Every thirty seconds, request the last five minutes. The overlap costs you nothing because your writes are idempotent, and it means a single missed tick self-heals on the next one rather than becoming a permanent hole.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Deduplicate on <code>incomingId<\/code>.<\/strong> It is the only field on the row that reads like a per-message primary key, it is a small integer in a string, and it is documented as belonging to the incoming message. Make it a unique constraint and upsert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Page to exhaustion inside every tick.<\/strong> Set <code>limit=200<\/code> and keep requesting until a short page arrives. A busy bot after a broadcast can produce far more than 200 inbound messages in five minutes, and a poller that reads only the first page will fall permanently behind at exactly the moment the conversation matters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Track your own lag.<\/strong> On every tick, record the difference between now and the newest <code>receivedAt<\/code> you saw. That single number tells you whether the poller is keeping up, and it is the metric that will page you before a customer complains that nobody answered.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Results are documented as newest first, so within a page you are reading in descending time order. Do not rely on that ordering to terminate; rely on the page length and your date bound.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The mobile number is your only join key back to a customer, which means an RCS conversation is identified by handset, not by thread. If the same person messages two different bots you will see two unrelated streams. Key your conversation state on the pair <code>(botId, mobileNo)<\/code>, not on <code>mobileNo<\/code> alone.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"summary-endpoint\" class=\"wp-block-heading\">The Summary Endpoint and Reconciliation<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/summary<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Legacy alias <code>\/rest\/rcs\/summary<\/code>. Same auth and paging model. Optional filters: <code>action<\/code>, <code>fromDate<\/code>, <code>toDate<\/code>, <code>botId<\/code>, <code>page<\/code>, <code>limit<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented row:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"botName\": \"YourBot\",\n  \"direction\": 1,\n  \"directionLabel\": \"A2P\",\n  \"countryName\": \"India\",\n  \"msgTypeLabel\": \"RICH\",\n  \"total\": 3,\n  \"success\": 0,\n  \"read\": 0,\n  \"failed\": 3,\n  \"notSent\": 0,\n  \"others\": 0,\n  \"refund\": 0,\n  \"invalid\": 0,\n  \"rcsEnabled\": 0,\n  \"clicked\": 0,\n  \"summaryTime\": 1788373800000,\n  \"summaryTimeFormatted\": \"2026-09-03 00:00:00\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Every counter is an <strong>unquoted integer<\/strong>. This is a meaningful difference from the SMS summary endpoint, where the counters come back as quoted strings with a single unquoted exception. If you have a shared summary ingestion path across channels, it needs a per-channel coercion step, and the RCS side is the well-behaved one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rows are grouped by bot, direction, country and message type, with <code>summaryTime<\/code> at midnight in the sample, which reads as daily buckets.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Nine outcome counters sit under <code>total<\/code>: <code>success<\/code>, <code>read<\/code>, <code>failed<\/code>, <code>notSent<\/code>, <code>others<\/code>, <code>refund<\/code>, <code>invalid<\/code>, <code>rcsEnabled<\/code>, <code>clicked<\/code>. Three of them have no SMS analogue and are worth understanding before you build a dashboard on this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>read<\/code> and <code>clicked<\/code> are engagement counters, and they are subsets of <code>success<\/code> rather than peers of it. A message that was delivered, read and clicked increments all three. Do not sum the outcome counters and expect <code>total<\/code>; you will overcount every engaged message twice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>rcsEnabled<\/code> is the interesting one. Given that the delivery report&#8217;s headline failure mode is <code>globalErrorCode<\/code> 5007, &#8220;RCS Not Enabled&#8221;, a counter by that name is almost certainly your reach measurement: how many of the recipients in this bucket could actually receive RCS. Track it as a ratio against <code>total<\/code> and watch it over months. That single ratio is the number that tells you whether RCS is becoming your primary channel or remaining a supplement, and it is the number your fallback configuration should be tuned against.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>refund<\/code> implies failed messages are credited back, which is consistent with the failed sample row carrying <code>amount<\/code> of <code>\"0.0000\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use this endpoint the way you would use any aggregate: as a <strong>cross-check, never as a source<\/strong>. It has no recipient granularity, so you cannot answer &#8220;did this customer get their message&#8221; from it. What you can do, cheaply, every morning, is pull yesterday&#8217;s summary and compare <code>total<\/code> against the row count your own ingest wrote from the delivery report for the same window. Equal is healthy. Your count lower means your ingest dropped rows. Your count higher means you double-inserted. That two-line comparison catches the majority of ingest faults before anyone downstream sees bad numbers, and it costs one API call a day.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"endpoint-reference\" class=\"wp-block-heading\">Endpoint Reference Table<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Purpose<\/th><th>Method<\/th><th>Endpoint<\/th><th>Format param<\/th><th>Key request fields<\/th><th>Response list key<\/th><\/tr><\/thead><tbody><tr><td>Send, one or more numbers<\/td><td>POST<\/td><td><code>RCSApi\/send<\/code><\/td><td><code>format<\/code><\/td><td><code>sendMethod=quick<\/code>, <code>mobile<\/code>, <code>botId<\/code>, <code>msg<\/code><\/td><td>none, flat object<\/td><\/tr><tr><td>Send, saved group<\/td><td>POST<\/td><td><code>RCSApi\/send<\/code><\/td><td><code>format<\/code><\/td><td><code>sendMethod=group<\/code>, <code>group<\/code>, <code>botId<\/code>, <code>msg<\/code><\/td><td>none, flat object<\/td><\/tr><tr><td>Send, file upload<\/td><td>POST<\/td><td><code>RCSApi\/send<\/code><\/td><td><code>format<\/code><\/td><td><code>sendMethod=bulkupload<\/code>, <code>phoneHeader<\/code>, <code>customUpload<\/code>, <code>botId<\/code>, <code>msg<\/code><\/td><td>none, flat object<\/td><\/tr><tr><td>Create template<\/td><td>POST<\/td><td><code>RCSApi\/template\/create<\/code><\/td><td>not documented<\/td><td><code>botId<\/code>, <code>templateData<\/code><\/td><td>none, flat object<\/td><\/tr><tr><td>List templates<\/td><td>GET<\/td><td><code>RCSApi\/template\/list<\/code><\/td><td>not documented<\/td><td><code>botId<\/code><\/td><td><code>templates<\/code><\/td><\/tr><tr><td>Delete template<\/td><td>POST<\/td><td><code>RCSApi\/template\/delete<\/code><\/td><td>not documented<\/td><td><code>botId<\/code>, <code>templateName<\/code><\/td><td>none, flat object<\/td><\/tr><tr><td>List bots<\/td><td>GET<\/td><td><code>rest\/rcs\/v1\/bots<\/code><\/td><td><code>output<\/code><\/td><td><code>action<\/code><\/td><td><code>botsList<\/code><\/td><\/tr><tr><td>Delivery reports<\/td><td>GET<\/td><td><code>rest\/rcs\/v1\/dlr<\/code><\/td><td><code>output<\/code><\/td><td><code>fromDate<\/code>, <code>toDate<\/code>, <code>uuId<\/code>, <code>botId<\/code>, <code>campaignName<\/code>, <code>templateName<\/code>, <code>globalErrorCode<\/code>, <code>page<\/code>, <code>limit<\/code><\/td><td><code>dlrList<\/code><\/td><\/tr><tr><td>Inbound messages<\/td><td>GET<\/td><td><code>rest\/rcs\/v1\/inbox<\/code><\/td><td><code>output<\/code><\/td><td><code>fromDate<\/code>, <code>toDate<\/code>, <code>mobileNo<\/code>, <code>botId<\/code>, <code>page<\/code>, <code>limit<\/code><\/td><td><code>inboxList<\/code><\/td><\/tr><tr><td>Aggregate counts<\/td><td>GET<\/td><td><code>rest\/rcs\/v1\/summary<\/code><\/td><td><code>output<\/code><\/td><td><code>fromDate<\/code>, <code>toDate<\/code>, <code>botId<\/code>, <code>page<\/code>, <code>limit<\/code><\/td><td><code>summaryList<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">All paths are relative to <code>https:\/\/unify.smsgateway.center\/<\/code>. The four <code>rest\/rcs\/v1\/<\/code> endpoints also answer on the same path with the <code>v1<\/code> segment removed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note the middle three rows. The template endpoints document neither <code>format<\/code> nor <code>output<\/code> in their parameter tables, and their samples pass neither. They return JSON regardless. Send nothing rather than guessing, and parse JSON unconditionally on that family.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"success-codes\" class=\"wp-block-heading\">Success Codes Across the RCS Families<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tabulating the success <code>code<\/code> or <code>statusCode<\/code> value across a CRUD family is worth doing on this platform, because the SMS group family contains an endpoint that returns <code>224<\/code> on success while its seven siblings return <code>200<\/code>. Any client testing for <code>200<\/code> on that endpoint has a live bug.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The RCS family, side by side:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Endpoint<\/th><th>Success field<\/th><th>Success value<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>RCSApi\/send<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Quoted string. <code>status<\/code> is <code>\"success\"<\/code>.<\/td><\/tr><tr><td><code>RCSApi\/template\/create<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td><code>reason<\/code> reads &#8220;Accepted for processing.&#8221;<\/td><\/tr><tr><td><code>RCSApi\/template\/list<\/code><\/td><td>none<\/td><td>none<\/td><td>No status field of any kind in the response.<\/td><\/tr><tr><td><code>RCSApi\/template\/delete<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td><code>reason<\/code> reads &#8220;Template deleted successfully.&#8221;<\/td><\/tr><tr><td><code>rest\/rcs\/v1\/bots<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Error case documented as <code>\"403\"<\/code>.<\/td><\/tr><tr><td><code>rest\/rcs\/v1\/dlr<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Error case documented as <code>\"403\"<\/code>.<\/td><\/tr><tr><td><code>rest\/rcs\/v1\/inbox<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Error case documented as <code>\"403\"<\/code>.<\/td><\/tr><tr><td><code>rest\/rcs\/v1\/summary<\/code><\/td><td><code>statusCode<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Error case documented as <code>\"403\"<\/code>.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Consistent, with the single exception of <code>template\/list<\/code>, which returns no status field at all. So the RCS family does not repeat the SMS platform&#8217;s anomaly, and <code>statusCode == \"200\"<\/code> is a safe success test everywhere it exists.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two rules survive that finding anyway. First, <code>statusCode<\/code> is a <strong>quoted string<\/strong> on every one of these, so compare against <code>\"200\"<\/code> and not <code>200<\/code>; a loosely typed comparison will pass in PHP and fail in Python. Second, for <code>template\/list<\/code>, test on the presence and type of the <code>templates<\/code> key instead: a successful call returns an array, and anything else is a failure whatever the HTTP status says.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"rcs-vs-sms\" class=\"wp-block-heading\">RCS Against SMS: A Field-Level Comparison<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This table is the porting guide. Every row is a place where code written against the SMS API will not work unchanged.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concept<\/th><th>SMS API<\/th><th>RCS API<\/th><\/tr><\/thead><tbody><tr><td>Send endpoint<\/td><td><code>SMSApi\/send<\/code><\/td><td><code>RCSApi\/send<\/code><\/td><\/tr><tr><td>Format parameter<\/td><td><code>output<\/code><\/td><td><code><code>output<\/code><\/code><\/td><\/tr><tr><td>Sender identity<\/td><td><code>senderid<\/code>, alphanumeric header<\/td><td><code>botId<\/code>, a registered agent<\/td><\/tr><tr><td>Message body<\/td><td><code>msg<\/code> as plain text<\/td><td><code>msg<\/code> as a JSON document naming a template code<\/td><\/tr><tr><td>Template variable syntax<\/td><td><code>{#var#}<\/code> for DLT<\/td><td><code>[name]<\/code> square brackets<\/td><\/tr><tr><td>Send response shape<\/td><td>Four different key sets by <code>sendMethod<\/code><\/td><td>One key set for all three methods<\/td><\/tr><tr><td>Platform message id<\/td><td><code>transactionId<\/code><\/td><td><code>transactionId<\/code>, same name<\/td><\/tr><tr><td>Caller-supplied reference<\/td><td>none<\/td><td><code>identifier<\/code><\/td><\/tr><tr><td>Reporting base path<\/td><td><code>SMSApi\/reports\/status<\/code><\/td><td><code>rest\/rcs\/v1\/dlr<\/code><\/td><\/tr><tr><td>Reporting method<\/td><td>GET or POST<\/td><td>GET<\/td><\/tr><tr><td>Reporting paging<\/td><td>Cursor, via <code>cursors.next<\/code><\/td><td>Offset, via <code>page<\/code> and <code>limit<\/code><\/td><\/tr><tr><td>Report envelope<\/td><td>Wrapped in <code>response<\/code>, <code>count<\/code> is an object<\/td><td>Flat, <code>totalRecords<\/code> is a scalar<\/td><\/tr><tr><td>Report list key<\/td><td><code>reports_dlrList<\/code><\/td><td><code>dlrList<\/code><\/td><\/tr><tr><td>Delivery outcome field<\/td><td><code>status<\/code><\/td><td><code>deliveryStatus<\/code><\/td><\/tr><tr><td>Numeric error field<\/td><td><code>globalErrorCode<\/code><\/td><td><code>globalErrorCode<\/code>, same name<\/td><\/tr><tr><td>Recipient number type<\/td><td>Unquoted number<\/td><td>Quoted string<\/td><\/tr><tr><td>Cost field type<\/td><td><code>amount<\/code> unquoted float<\/td><td><code>amount<\/code> quoted string, four decimals<\/td><\/tr><tr><td>Read and click tracking<\/td><td>none<\/td><td><code>readTime<\/code> and <code>clickedTime<\/code> on every row<\/td><\/tr><tr><td>Inbound messages<\/td><td>Separate shortcode and webhook surfaces<\/td><td><code>rest\/rcs\/v1\/inbox<\/code>, polling<\/td><\/tr><tr><td>Segment and part counting<\/td><td>Central to billing<\/td><td>Not applicable<\/td><\/tr><tr><td>API key auth on reporting<\/td><td>Header on the SMS API<\/td><td>Header <code>apikey<\/code> on <code>rest\/rcs\/v1\/<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The two rows to internalise are the delivery outcome field and the paging model. Everything else is a rename you will notice within an hour of testing. Those two produce code that runs, returns 200, and quietly gives you the wrong answer.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"fallback\" class=\"wp-block-heading\">Fallback Is the Migration Path, Not Replacement<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The framing of &#8220;migrating from SMS to RCS&#8221; is wrong, and the delivery report proves it. <code>globalErrorCode<\/code> 5007 with <code>cause<\/code> &#8220;RCS Not Enabled&#8221; is not a transient error you retry through. It is the platform telling you that this recipient, right now, cannot receive an RCS message. Retrying the same channel produces the same result.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the target architecture is both channels, with a rule deciding which one carries a given message. <a href=\"https:\/\/www.smsgatewaycenter.com\/multi-channel-fallback\/\">Multi-Channel Fallback<\/a> is the platform mechanism for exactly this. Its semantics are documented in the knowledge base: trigger error codes control when the platform moves to the next channel, and by default any failure on a channel other than the last can trigger the next step. The detail matters, and the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-are-trigger-error-codes-in-multi-channel-fallback\/\">trigger error codes article<\/a> is the page to read before configuring anything.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three concrete patterns, in increasing order of engineering effort.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pattern one: platform-configured fallback, RCS first, SMS second.<\/strong> You configure the chain once and send to it. Simplest to operate, and the right starting point. Because leaving the trigger codes empty means any RCS failure moves the message to SMS, a 5007 falls through to SMS automatically without you writing any routing logic. For time-critical messages, and particularly for OTP, the empty-trigger-codes default is the recommended configuration precisely because you want any failure to advance immediately rather than waiting on a code match.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pattern two: application-side routing on a reach cache.<\/strong> Send RCS, ingest the delivery report, and record per recipient whether RCS succeeded or returned 5007. On the next send to that recipient, choose the channel from that record. This saves you the failed RCS attempt and its latency, which matters for OTP where the fallback hop is dead time the user spends staring at an empty input field. The cost is a reach table you have to expire, because RCS availability changes when a customer changes handset or their operator enables the service. Expire entries after a few weeks and let them re-learn.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pattern three: content-aware routing.<\/strong> Some messages are worth RCS and some are not. A rich card with a tracking button is worth the attempt. A one-line balance alert is not, and sending it as RCS buys nothing while adding a failure mode. Tag messages by purpose at the application boundary and route on the tag. This is the pattern that actually controls cost, because it stops you from paying rich-message rates for content that gains nothing from being rich.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Whichever pattern you choose, instrument the split. Your dashboard needs, per day, the count of messages attempted on RCS, the count that fell back, and the reason distribution of the fallbacks. The <code>rcsEnabled<\/code> counter on the summary endpoint gives you the platform&#8217;s own view of the same question, and comparing the two is how you find out that your reach cache has gone stale.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One deliberate exception. Do not put OTP on RCS-first unless you have measured the fallback latency and found it acceptable. The reliability argument in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/otp-delivery-india-reliability-checklist\/\">OTP delivery checklist<\/a> applies with more force here, because a fallback hop adds a step to a flow where every second measurably reduces completion. RCS is an excellent channel for the confirmation that follows a successful login. It is a questionable first hop for the code itself.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"first-send\" class=\"wp-block-heading\">Your First RCS Send in Six Steps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step one. Confirm entitlement.<\/strong> Call <code>rest\/rcs\/v1\/bots<\/code>. A <code>statusCode<\/code> of <code>\"403\"<\/code> with the reason about RCS API access means the product is not enabled on the account and no amount of correct code will fix it. This call is also your health check; wire it into your deployment smoke test.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step two. Pick a bot and record whether it is a sandbox.<\/strong> Take <code>botId<\/code> from the list. Assert on <code>isSandbox<\/code> matching your environment before the process finishes starting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step three. Create a template and wait for it.<\/strong> POST to <code>RCSApi\/template\/create<\/code> with a <code>text_message<\/code> type first, before attempting a card or carousel. Then poll <code>RCSApi\/template\/list<\/code> until the template appears with <code>status<\/code> of <code>1<\/code>. Do not skip the poll.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step four. Send one message to a number you control.<\/strong> Use <code>sendMethod=quick<\/code> with a single number. Capture the entire response body verbatim to a log, not just the fields you expect. Record the <code>transactionId<\/code> and the <code>identifier<\/code> you sent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step five. Read the delivery report for that one message.<\/strong> Call <code>rest\/rcs\/v1\/dlr<\/code> filtered by <code>uuId<\/code> equal to the <code>transactionId<\/code> you recorded. Compare the returned row field by field against what you submitted: does <code>campaignName<\/code> hold your <code>identifier<\/code>, does the double-encoded <code>message<\/code> field match your payload, what is in <code>globalErrorCode<\/code>. This one comparison, on one message you control, establishes the whole contract empirically and takes an afternoon.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step six. Only now build the batch path.<\/strong> Move to <code>group<\/code> or <code>bulkupload<\/code>, add the poller, add the ingest. Everything after step five is scale; everything before it is contract.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"working-code\" class=\"wp-block-heading\">Working Code: cURL, Python, PHP, Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Credentials in every sample below are placeholders. Never put a live password in a query string, and never ship any of this into a browser or a mobile app, because these calls carry account credentials and belong on your server.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL, quick send<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>curl --location --request POST 'https:\/\/unify.smsgateway.center\/RCSApi\/send' \\\n  --header 'Content-Type: application\/x-www-form-urlencoded' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'password=YOUR_PASSWORD' \\\n  --data-urlencode 'sendMethod=quick' \\\n  --data-urlencode 'msgType=text' \\\n  --data-urlencode 'format=json' \\\n  --data-urlencode 'botId=YOUR_BOT_ID' \\\n  --data-urlencode 'mobile=919999999999' \\\n  --data-urlencode 'msg={\"contentMessage\":{\"templateMessage\":{\"templateCode\":\"WELCOME_TEMPLATE\"}},\"ttl\":\"86400s\"}' \\\n  --data-urlencode 'identifier=svc-orders.welcome.v1'<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Python, send and then reconcile<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import json\nimport time\nfrom decimal import Decimal\n\nimport requests\n\nBASE = \"https:\/\/unify.smsgateway.center\"\nUSERID = \"YourUsername\"\nPASSWORD = \"YOUR_PASSWORD\"\nBOT_ID = \"YOUR_BOT_ID\"\n\n\ndef send_rcs(mobile, template_code, identifier, ttl_seconds=86400):\n    payload = {\n        \"contentMessage\": {\"templateMessage\": {\"templateCode\": template_code}},\n        \"ttl\": f\"{ttl_seconds}s\",\n    }\n    resp = requests.post(\n        f\"{BASE}\/RCSApi\/send\",\n        data={\n            \"userid\": USERID,\n            \"password\": PASSWORD,\n            \"sendMethod\": \"quick\",\n            \"msgType\": \"text\",\n            \"format\": \"json\",              # note: format, not output\n            \"botId\": BOT_ID,\n            \"mobile\": mobile,\n            \"msg\": json.dumps(payload),\n            \"identifier\": identifier,\n        },\n        timeout=30,\n    )\n    resp.raise_for_status()\n    body = resp.json()\n\n    # statusCode is a quoted string on every RCSApi endpoint.\n    if body.get(\"statusCode\") != \"200\":\n        raise RuntimeError(f\"send rejected: {body.get('reason')} ({body.get('statusCode')})\")\n\n    invalid = body.get(\"invalidMobile\") or \"\"\n    if invalid:\n        # Only signal that recipients were dropped before submission.\n        print(f\"WARNING invalid recipients: {invalid}\")\n\n    # Nineteen digits. Keep it a string, always.\n    return str(body&#91;\"transactionId\"])\n\n\ndef fetch_dlr_page(from_date, to_date, page=1, limit=200):\n    resp = requests.get(\n        f\"{BASE}\/rest\/rcs\/v1\/dlr\",\n        params={\n            \"userid\": USERID,\n            \"password\": PASSWORD,\n            \"output\": \"json\",             # note: output, not format\n            \"action\": \"list\",\n            \"fromDate\": from_date,\n            \"toDate\": to_date,\n            \"page\": page,\n            \"limit\": limit,\n        },\n        timeout=60,\n    )\n    resp.raise_for_status()\n    body = resp.json()\n    if body.get(\"statusCode\") != \"200\":\n        raise RuntimeError(f\"dlr call failed: {body.get('reason')}\")\n    return body\n\n\ndef iter_dlr(from_date, to_date, limit=200):\n    \"\"\"Page to exhaustion over a closed window.\"\"\"\n    page = 1\n    seen = 0\n    while True:\n        body = fetch_dlr_page(from_date, to_date, page, limit)\n        rows = body.get(\"dlrList\") or &#91;]\n        for row in rows:\n            yield row\n        seen += len(rows)\n        total = body.get(\"totalRecords\") or 0\n        # Two termination conditions, not one.\n        if len(rows) &lt; limit or seen &gt;= total:\n            return\n        page += 1\n        time.sleep(0.2)\n\n\ndef normalise(row):\n    \"\"\"Map a raw dlr row onto columns you can safely store.\"\"\"\n\n    def epoch_or_none(value):\n        # Zero means the event never happened.\n        return int(value) if value else None\n\n    submit = epoch_or_none(row.get(\"submitTime\"))\n    delivered = epoch_or_none(row.get(\"deliveryTime\"))\n    latency_ms = None\n    if submit is not None and delivered is not None:\n        latency_ms = max(0, delivered - submit)   # clamp, never trust clock order\n\n    payload = None\n    raw = row.get(\"message\")\n    if raw:\n        try:\n            payload = json.loads(raw)             # double-encoded JSON\n        except (ValueError, TypeError):\n            payload = None\n\n    return {\n        \"uu_id\": str(row&#91;\"uuId\"]),\n        \"mobile_no\": str(row&#91;\"mobileNo\"]),\n        \"bot_name\": row.get(\"botName\"),\n        \"campaign_name\": row.get(\"campaignName\") or None,\n        \"global_error_code\": int(row.get(\"globalErrorCode\", 0)),\n        \"delivery_status\": row.get(\"deliveryStatus\"),\n        \"cause\": row.get(\"cause\"),\n        \"amount\": Decimal(row.get(\"amount\") or \"0\"),   # quoted string, four decimals\n        \"submit_time_ms\": submit,\n        \"delivery_time_ms\": delivered,\n        \"read_time_ms\": epoch_or_none(row.get(\"readTime\")),\n        \"clicked_time_ms\": epoch_or_none(row.get(\"clickedTime\")),\n        \"delivery_latency_ms\": latency_ms,\n        \"sent_payload\": payload,\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Upsert the output of <code>normalise<\/code> on the pair <code>(uu_id, mobile_no)<\/code>. That makes re-running a window free and makes the overlap in your poller harmless.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PHP, listing templates and handling the missing envelope<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\nfunction rcsListTemplates(string $userid, string $password, string $botId): array\n{\n    $url = 'https:\/\/unify.smsgateway.center\/RCSApi\/template\/list?' . http_build_query(&#91;\n        'userid'   =&gt; $userid,\n        'password' =&gt; $password,\n        'botId'    =&gt; $botId,\n    ]);\n\n    $ch = curl_init($url);\n    curl_setopt_array($ch, &#91;\n        CURLOPT_RETURNTRANSFER =&gt; true,\n        CURLOPT_TIMEOUT        =&gt; 30,\n    ]);\n    $raw = curl_exec($ch);\n    if ($raw === false) {\n        throw new RuntimeException('template\/list transport error: ' . curl_error($ch));\n    }\n    curl_close($ch);\n\n    $body = json_decode($raw, true);\n\n    \/\/ This endpoint returns no status, no statusCode and no reason.\n    \/\/ The only success test available is the shape of the templates key.\n    if (!is_array($body) || !isset($body&#91;'templates']) || !is_array($body&#91;'templates'])) {\n        throw new RuntimeException('template\/list returned an unexpected shape');\n    }\n\n    $out = &#91;];\n    foreach ($body&#91;'templates'] as $row) {\n        $model = null;\n        if (!empty($row&#91;'data'])) {\n            \/\/ data is a JSON string inside JSON. One bad row must not kill the list.\n            $decoded = json_decode($row&#91;'data'], true);\n            $model = is_array($decoded) ? $decoded : null;\n        }\n\n        $out&#91;] = &#91;\n            \/\/ The name IS the templateCode you send with.\n            'template_code' =&gt; $row&#91;'name'] ?? null,\n            'bot_name'      =&gt; $row&#91;'botName'] ?? null,\n            'type'          =&gt; $row&#91;'type'] ?? null,\n            \/\/ Casing differs between what you submit and what comes back.\n            'category'      =&gt; isset($row&#91;'category']) ? strtoupper($row&#91;'category']) : null,\n            'is_active'     =&gt; (int)($row&#91;'status'] ?? 0) === 1,\n            'created_ms'    =&gt; isset($row&#91;'creationTime']) ? (int)$row&#91;'creationTime'] : null,\n            'updated_ms'    =&gt; isset($row&#91;'updationTime']) ? (int)$row&#91;'updationTime'] : null,\n            'model'         =&gt; $model,\n        ];\n    }\n\n    return $out;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js, the large-integer hazard<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ The transactionId and uuId values are nineteen digits.\n\/\/ JSON.parse will silently round them past Number.MAX_SAFE_INTEGER.\n\/\/ Pull them out of the raw body BEFORE parsing.\n\nfunction extractStringField(rawBody, field) {\n  const match = rawBody.match(new RegExp(`\"${field}\"\\\\s*:\\\\s*\"(&#91;^\"]+)\"`));\n  return match ? match&#91;1] : null;\n}\n\nasync function sendRcs({ mobile, templateCode, identifier }) {\n  const params = new URLSearchParams({\n    userid: process.env.SGC_USERID,\n    password: process.env.SGC_PASSWORD,\n    sendMethod: 'quick',\n    msgType: 'text',\n    format: 'json',                 \/\/ format here, output on the rest endpoints\n    botId: process.env.SGC_BOT_ID,\n    mobile,\n    msg: JSON.stringify({\n      contentMessage: { templateMessage: { templateCode } },\n      ttl: '86400s',\n    }),\n    identifier,\n  });\n\n  const res = await fetch('https:\/\/unify.smsgateway.center\/RCSApi\/send', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n    body: params,\n  });\n\n  const rawBody = await res.text();\n  const body = JSON.parse(rawBody);\n\n  if (body.statusCode !== '200') {\n    throw new Error(`RCS send rejected: ${body.reason} (${body.statusCode})`);\n  }\n\n  return {\n    \/\/ Read the digits from the raw text, not from the parsed object.\n    transactionId: extractStringField(rawBody, 'transactionId'),\n    invalidMobile: body.invalidMobile || '',\n    reason: body.reason,\n  };\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">There is no official Node.js or Java SDK for this platform. The two published SDKs are PHP and Python, listed on the <a href=\"https:\/\/www.smsgatewaycenter.com\/download\/\">downloads page<\/a>, and neither is documented as covering the RCS endpoints, so treat the code above as the reference implementation for those languages. A Postman collection for the SMS surface is available and worth importing for the shared authentication patterns, but build the RCS requests by hand from the tables in this reference.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"ten-mistakes\" class=\"wp-block-heading\">Ten Mistakes That Cost a Week<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Sending <code>output=json<\/code> to an <code>RCSApi\/<\/code> endpoint.<\/strong> The parameter is <code>format<\/code> on that family. Symptom: a response in an unexpected format, or a parse error your logs attribute to the network.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Sending <code>format=json<\/code> to a <code>rest\/rcs\/v1\/<\/code> endpoint.<\/strong> The parameter is <code>output<\/code> there, and it is documented as required. Same class of symptom, opposite direction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Asserting on <code>payload[\"status\"]<\/code> before returning from your HTTP helper.<\/strong> <code>RCSApi\/template\/list<\/code> has no <code>status<\/code> field. Every successful template listing will throw.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Comparing <code>statusCode<\/code> to the integer <code>200<\/code>.<\/strong> It is the quoted string <code>\"200\"<\/code> everywhere it appears. Passes in PHP, fails in Python and TypeScript.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Reading the delivery outcome from a field called <code>status<\/code>.<\/strong> On the RCS delivery row, the outcome is <code>deliveryStatus<\/code>. The field named <code>status<\/code> is the envelope-level call outcome. The names are swapped relative to the SMS API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Letting <code>JSON.parse<\/code> touch <code>transactionId<\/code>.<\/strong> Nineteen digits, silently rounded, and the corrupted value looks entirely plausible. Every subsequent delivery report lookup returns nothing and you spend two days suspecting the platform.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Treating &#8220;Accepted for processing&#8221; as an approved template.<\/strong> Poll <code>template\/list<\/code> for <code>status<\/code> of <code>1<\/code> before routing production traffic at a new template code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. Carrying <code>{#var#}<\/code> substitution code over from DLT.<\/strong> RCS placeholders are <code>[name]<\/code>. Your substitution will no-op and ship the literal placeholder to a customer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Retrying a 5007.<\/strong> &#8220;RCS Not Enabled&#8221; is a fact about the recipient, not a transient failure. Fall back to SMS; do not retry the same channel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Summing the summary counters and expecting <code>total<\/code>.<\/strong> <code>read<\/code> and <code>clicked<\/code> are subsets of <code>success<\/code>, not peers of it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"decision-matrix\" class=\"wp-block-heading\">A Decision Matrix for Channel Choice<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Twelve dimensions across the three realistic options. Read the rows that matter to your use case rather than counting the column scores.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Dimension<\/th><th>SMS only<\/th><th>RCS only<\/th><th>RCS with SMS fallback<\/th><\/tr><\/thead><tbody><tr><td>Reach<\/td><td>Effectively universal<\/td><td>Limited to RCS-capable handsets, and 5007 tells you where it is not<\/td><td>Effectively universal<\/td><\/tr><tr><td>Integration effort<\/td><td>Lowest, one API<\/td><td>Moderate, two API generations and a bot and template estate<\/td><td>Highest, both paths plus a routing rule<\/td><\/tr><tr><td>Message richness<\/td><td>Text only<\/td><td>Cards, carousels, media, tappable suggestions<\/td><td>Rich where possible, plain where not<\/td><\/tr><tr><td>Brand identity<\/td><td>Six-character alphanumeric header<\/td><td>Verified agent with a name and a logo<\/td><td>Verified where possible<\/td><\/tr><tr><td>Read tracking<\/td><td>None<\/td><td><code>readTime<\/code> on every delivery row<\/td><td>Partial, on the RCS half<\/td><\/tr><tr><td>Click tracking<\/td><td>Only via link shorteners<\/td><td><code>clickedTime<\/code> on every delivery row<\/td><td>Partial, on the RCS half<\/td><\/tr><tr><td>Inbound handling<\/td><td>Shortcode and webhook surfaces<\/td><td>Polling <code>rest\/rcs\/v1\/inbox<\/code><\/td><td>Both, on separate paths<\/td><\/tr><tr><td>Latency for OTP<\/td><td>Single hop, well understood<\/td><td>Single hop when reachable<\/td><td>Two hops on fallback, measurably slower<\/td><\/tr><tr><td>Cost predictability<\/td><td>Segment arithmetic, well documented<\/td><td>Per-message, read from the portal<\/td><td>Mixed, requires per-channel attribution<\/td><\/tr><tr><td>Template governance<\/td><td>DLT registration, <code>{#var#}<\/code><\/td><td>Bot-scoped RCS templates, <code>[name]<\/code>, no update endpoint<\/td><td>Two template estates to keep in step<\/td><\/tr><tr><td>Delivery reconciliation<\/td><td>Cursor paging, wrapped envelope<\/td><td>Offset paging, flat envelope<\/td><td>Two ingests into one table<\/td><\/tr><tr><td>Best fit<\/td><td>Alerts, OTP, anything time-critical<\/td><td>Marketing and post-transaction engagement with a known-reachable audience<\/td><td>Almost every production estate<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The honest summary is that the third column wins for most organisations, and the reason is in the reach row rather than the richness row. RCS is worth adding because of what it lets you do with the recipients it reaches, not because it replaces anything.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"checklist\" class=\"wp-block-heading\">Production Readiness Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Entitlement and configuration:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>rest\/rcs\/v1\/bots<\/code> returns <code>statusCode<\/code> <code>\"200\"<\/code> from the production environment<\/li>\n\n\n\n<li>The <code>403<\/code> reason string about RCS API access is handled distinctly from an empty result<\/li>\n\n\n\n<li><code>isSandbox<\/code> is asserted against the environment at process start<\/li>\n\n\n\n<li><code>botId<\/code> is loaded from configuration, never hardcoded in a send call<\/li>\n\n\n\n<li>The bots list is cached and refreshed on a schedule, not per send<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Authentication:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <code>apikey<\/code> header is used on all <code>rest\/rcs\/v1\/<\/code> calls rather than a password in a query string<\/li>\n\n\n\n<li><code>userid<\/code> is sent alongside the <code>apikey<\/code> header, since it remains required<\/li>\n\n\n\n<li>No credential appears in any URL that reaches an access log<\/li>\n\n\n\n<li>No RCS call is made from browser or mobile client code<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Templates:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Template definitions live in version control as immutable, versioned artefacts<\/li>\n\n\n\n<li>Deployment polls <code>template\/list<\/code> for <code>status<\/code> of <code>1<\/code> before enabling a new template code<\/li>\n\n\n\n<li>Category comparisons are case-insensitive<\/li>\n\n\n\n<li>The <code>data<\/code> field is parsed in its own try-catch, per row<\/li>\n\n\n\n<li><code>fallbackText<\/code> is populated with a standalone message on every template<\/li>\n\n\n\n<li>The <code>[placeholder]<\/code> substitution has its own tests and asserts zero unreplaced brackets before send<\/li>\n\n\n\n<li>Deleting a template is preceded by a check for in-flight sends referencing it<\/li>\n\n\n\n<li><code>mediaUrl<\/code> values point at stable, non-expiring, cacheable assets<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Sending:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>format=json<\/code> on every <code>RCSApi\/<\/code> call<\/li>\n\n\n\n<li><code>statusCode<\/code> is compared against the string <code>\"200\"<\/code><\/li>\n\n\n\n<li><code>invalidMobile<\/code> is logged at warning level with the request recipient count<\/li>\n\n\n\n<li><code>transactionId<\/code> is stored as text everywhere<\/li>\n\n\n\n<li><code>identifier<\/code> is populated on every send with a structured, service-scoped value<\/li>\n\n\n\n<li><code>ttl<\/code> is set per message purpose, not left at a copied default<\/li>\n\n\n\n<li>The recipient list is persisted at submission, never derived from the response<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Delivery reporting:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>output=json<\/code> on every <code>rest\/rcs\/v1\/<\/code> call<\/li>\n\n\n\n<li>Paging terminates on both a short page and the <code>totalRecords<\/code> bound<\/li>\n\n\n\n<li>Queries are bounded to closed date windows<\/li>\n\n\n\n<li>Ingest upserts on <code>uuId<\/code> plus <code>mobileNo<\/code><\/li>\n\n\n\n<li>All four timestamps are stored, with zero mapped to null<\/li>\n\n\n\n<li>Latency calculations are clamped at zero and computed only on successful rows<\/li>\n\n\n\n<li>Branching is on <code>globalErrorCode<\/code>, never on <code>cause<\/code><\/li>\n\n\n\n<li>The outcome is read from <code>deliveryStatus<\/code>, not <code>status<\/code><\/li>\n\n\n\n<li><code>amount<\/code> is parsed as a decimal, not a float or an integer<\/li>\n\n\n\n<li>The double-encoded <code>message<\/code> field is parsed defensively<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Inbox:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The poller runs on a short interval with an overlapping window<\/li>\n\n\n\n<li>Deduplication is on <code>incomingId<\/code> with a unique constraint<\/li>\n\n\n\n<li>Every tick pages to exhaustion<\/li>\n\n\n\n<li>Poller lag against the newest <code>receivedAt<\/code> is a tracked metric<\/li>\n\n\n\n<li>Unrecognised <code>type<\/code> values are stored, not discarded<\/li>\n\n\n\n<li>Conversation state is keyed on <code>(botId, mobileNo)<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Operations:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A daily job compares summary <code>total<\/code> against the delivery rows ingested for the same window<\/li>\n\n\n\n<li>The <code>rcsEnabled<\/code> ratio is tracked over time<\/li>\n\n\n\n<li>Fallback rate and fallback reason distribution are on a dashboard<\/li>\n\n\n\n<li>The delivery error code catalogue is diffed on a schedule<\/li>\n\n\n\n<li>OTP routing has been explicitly decided rather than defaulted<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"unspecified-behaviour\" class=\"wp-block-heading\">Unspecified Behaviour and How to Code Around It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A handful of behaviours in the RCS surface are not pinned down by anything you can read before you write code. Each item below gives you the choice that stays correct whichever way the behaviour actually resolves, so none of them require waiting on an answer before you ship.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Bound every report query to a closed window and make the ingest idempotent.<\/strong> Offset paging over a feed that is still receiving rows can shift entries between pages, and nothing tells you whether a page boundary moved under you. Query yesterday rather than today, upsert on <code>uuId<\/code> plus <code>mobileNo<\/code> rather than inserting, and re-running a window becomes free. You get the same answer whether the underlying set was stable or not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Poll for template activation instead of trusting the create response.<\/strong> <code>reason<\/code> reads &#8220;Accepted for processing&#8221; and <code>statusCode<\/code> reads <code>\"200\"<\/code>, which are two statements about intake rather than about approval. Poll <code>template\/list<\/code> until the row appears with <code>status<\/code> of <code>1<\/code>, then enable the code. This is correct whether approval takes two seconds or two days.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Delete templates by the exact <code>name<\/code> string you just read back.<\/strong> The delete parameter is called <code>templateName<\/code> and is described as accepting a name or an identifier, while the sample request passes a placeholder that reads like an encoded id. Read the list, take the <code>name<\/code> verbatim, pass that, and re-list to confirm the row is gone. The round-trip removes the ambiguity for the cost of one GET.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Populate <code>fallbackText<\/code> on every template.<\/strong> It appears in the create payload as an empty string and nothing describes when it renders. A well-written standalone message in that field cannot hurt you, and if it is what it sounds like, it saves a recipient from receiving nothing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Treat <code>mobile<\/code>, <code>invalidMobile<\/code> and <code>msgId<\/code> on the send response as optional.<\/strong> All three send methods publish the same seven-key response with a single number in <code>mobile<\/code> and an empty <code>msgId<\/code>, which cannot be the whole story for a group or file send. Bind <code>status<\/code>, <code>statusCode<\/code>, <code>reason<\/code> and <code>transactionId<\/code> as required and the other three as nullable, and keep your own recipient list. Your reconciliation then works identically whether the group send echoes one number, all of them, or none.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Keep <code>uuId<\/code> as your primary key and <code>campaignName<\/code> as an index.<\/strong> The sample delivery row returns <code>campaignName<\/code> as an empty string, and the filter&#8217;s matching semantics are not stated. Choose identifiers that are unique enough that a partial match would still be correct, free of characters that behave specially in a substring search, and never depend on <code>campaignName<\/code> being populated to find a row.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Dispatch on the inbox <code>type<\/code> field with an explicit default branch.<\/strong> The sample shows <code>type<\/code> of <code>text<\/code> with <code>typeLabel<\/code> of &#8220;FREE TEXT&#8221;, and suggestion postbacks must arrive somewhere. Store rows whose type you do not recognise rather than dropping them, and you will have the data on the day you add handling.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. Clamp every latency at zero and compute it only on successful rows.<\/strong> The sample delivery row carries a populated <code>deliveryTime<\/code> on a <code>FAILED<\/code> message, so the field marks the terminal state rather than a delivery. Filter on <code>globalErrorCode<\/code> before computing, and clamp the subtraction, and your dashboards stay honest regardless of clock skew between the two stamps.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Normalise <code>botId<\/code> to a string once, at the boundary.<\/strong> It is an unquoted integer in the bots list and a form field on every send. Convert on read, keep it a string internally, and you never compare <code>3<\/code> to <code>\"3\"<\/code> in a routing table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Verify the whole contract on one message before building the batch path.<\/strong> Send one RCS message to a handset you hold, capture the raw response body verbatim, then fetch the delivery report filtered by that <code>uuId<\/code> and compare every field against what you submitted. An afternoon spent on one message establishes the correlation chain, the timestamp semantics and the payload echo empirically, and the result is correct for your account rather than correct in general.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eleven. Read the price from the portal rather than from a constant.<\/strong> The one documented delivery row is a failed message priced at <code>\"0.0000\"<\/code>, which establishes only that failures are free. Pull the current rate from your wallet and your rate plan, quote from <a href=\"https:\/\/www.smsgatewaycenter.com\/rcs-pricing\/\">RCS pricing<\/a> when you need a published figure, and keep no rate literals in code.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is the RCS API the same as the SMS API with a different channel parameter?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No. It is a separate API on a separate base path. Sends go to <code>https:\/\/unify.smsgateway.center\/RCSApi\/send<\/code> rather than <code>SMSApi\/send<\/code>, they take <code>botId<\/code> rather than <code>senderid<\/code>, the format parameter is spelled <code>format<\/code> rather than <code>output<\/code>, and reporting lives on a third base path, <code>https:\/\/unify.smsgateway.center\/rest\/rcs\/v1\/<\/code>, with a different paging model and a different envelope.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do I need a separate account or a separate API key for RCS?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No separate account. The same <code>userid<\/code> and password work, and the same account API key works as an <code>apikey<\/code> header on the <code>rest\/rcs\/v1\/<\/code> endpoints. What you do need is the RCS product and RCS API access enabled on the account, which is what the documented <code>403<\/code> response with reason &#8220;RCS API access is not enabled for this account.&#8221; is telling you when you see it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is a bot and why can I not just use my sender ID?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">RCS replaces the alphanumeric sender header with a registered agent that carries a name and a logo and renders as a verified brand in the handset&#8217;s messaging app. Your <code>botId<\/code> comes from <code>rest\/rcs\/v1\/bots<\/code>, and the documentation states the bot must belong to the account you authenticate as. Sender IDs continue to apply to your SMS traffic and are unaffected.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I send free-form RCS text, or must everything be a template?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented send path requires an approved template code inside the <code>msg<\/code> payload at <code>contentMessage.templateMessage.templateCode<\/code>. Create templates through <code>RCSApi\/template\/create<\/code> and use the returned name as the code. The template can carry <code>[name]<\/code> style placeholders, so the content is parameterised even though the structure is fixed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the difference between <code>templateName<\/code>, <code>name<\/code> and <code>templateCode<\/code>?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">They are the same value under three field names. <code>template\/create<\/code> returns it as <code>templateName<\/code>, <code>template\/list<\/code> returns it as <code>name<\/code>, and the send payload wants it as <code>templateCode<\/code>. The list page states this directly. Write one adapter and use it everywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I correlate a send with its delivery report?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two ways. The reliable one is <code>transactionId<\/code> from the send response, which appears on the delivery row as <code>uuId<\/code> and is accepted as a request filter under either name. The convenient one is <code>identifier<\/code>, which you set on the send and filter on as <code>campaignName<\/code>. Use <code>uuId<\/code> as the primary key and <code>identifier<\/code> as a fast index for grouping by campaign or service.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Does RCS have a delivery webhook, or must I poll?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented RCS reporting surface is the GET endpoint <code>rest\/rcs\/v1\/dlr<\/code>, with <code>page<\/code> and <code>limit<\/code> paging up to 200 records. Build the poller, bound it to closed date windows, and make the ingest idempotent on <code>uuId<\/code> plus <code>mobileNo<\/code> so overlapping windows are harmless.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I see whether a customer read or tapped an RCS message?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, and this is the largest reporting gain over SMS. Every delivery row carries <code>readTime<\/code> and <code>clickedTime<\/code> as millisecond epochs, with zero meaning the event did not occur, plus preformatted string twins. That gives you delivered-to-read latency and read-to-click conversion per message with no instrumentation of your own.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why did my message fail with error code 5007?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>globalErrorCode<\/code> 5007 corresponds to <code>cause<\/code> &#8220;RCS Not Enabled&#8221;, meaning the recipient could not receive an RCS message. This is not a transient failure and retrying the same channel will produce the same result. Fall back to SMS, either through <a href=\"https:\/\/www.smsgatewaycenter.com\/multi-channel-fallback\/\">Multi-Channel Fallback<\/a> or through your own routing logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do two-way RCS conversations work?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Poll <code>rest\/rcs\/v1\/inbox<\/code>, which is documented as read-only, filter by <code>botId<\/code> and date range, and deduplicate on <code>incomingId<\/code>. To reply, send through <code>RCSApi\/send<\/code>. Key your conversation state on the pair of bot id and mobile number, because the same handset messaging two different bots produces two unrelated streams.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do RCS sends use my existing contact groups?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. <code>sendMethod=group<\/code> takes a saved group name, which is the same group estate you manage through the group and contact endpoints for SMS. You do not need a duplicate audience for RCS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is there an official SDK for RCS?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The platform publishes PHP and Python SDKs, listed on the <a href=\"https:\/\/www.smsgatewaycenter.com\/download\/\">downloads page<\/a>, and neither is documented as covering the RCS endpoints. There is no official Java, Node.js, Go, .NET or Ruby SDK for any part of this platform. Build against the endpoint tables directly; the request shapes are simple form posts and query strings.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should I move my OTP traffic to RCS?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Be careful here. RCS reaches only RCS-capable handsets, and the fallback hop when it does not adds latency to a flow where seconds measurably reduce completion. The safer pattern is SMS for the code itself and RCS for the richer confirmation that follows. If you do route OTP through a fallback chain, leaving the trigger error codes empty is the configuration that advances on any failure rather than waiting for a code match.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the fastest way to be confident my integration is correct?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Send one message to a handset you control, capture the entire raw response body, then query the delivery report filtered by that <code>uuId<\/code> and compare the returned row field by field against what you submitted. That single round-trip establishes the correlation chain, the timestamp semantics, the payload echo and the cost field empirically. It takes an afternoon and it is worth more than any amount of reading, including this page.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Suggested CTA<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ready to add RCS alongside your existing SMS traffic?<\/strong> RCS needs the product enabled on your account and a verified bot before the first call will work. <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">Talk to the SMSGatewayCenter team<\/a> about enabling RCS and provisioning an agent, or read the <a href=\"https:\/\/www.smsgatewaycenter.com\/rcs-messaging\/\">RCS Messaging product overview<\/a> and <a href=\"https:\/\/www.smsgatewaycenter.com\/rcs-pricing\/\">RCS pricing<\/a> first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Recent Articles<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/delivery-report-ingestion-system-of-record\/\">Delivery Report Ingestion: Building a System of Record, Not a Dashboard<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contact-groups-segments-data-model-group-send\/\">Contact Groups and Segments: The Data Model Behind a Group Send<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/campaign-splitting-one-send-several-transactions\/\">Campaign Splitting: What Happens When One Send Becomes Several Transactions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-preview-cost-estimation-before-sending\/\">Message Preview and Cost Estimation: Showing Users What They Are About to Send<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/oauth-messaging-connect-customer-sms-account\/\">OAuth for Messaging Platforms: Letting Customers Connect Their Own SMS Account<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A field reference for the SMSGatewayCenter RCS API written for engineers who already run SMS. Two API generations, three response envelopes, the bot and template model, the delivery report with read and click timestamps, the inbox, and how to run RCS alongside SMS with fallback rather than replacing it.<\/p>\n","protected":false},"author":118,"featured_media":2959,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[268],"tags":[2238,2231,2230,2234,2232,2237,2233,2236,811,2235,814,481,27],"class_list":["post-2958","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorials","tag-a2p-messaging","tag-messaging-api","tag-multi-channel-fallback-2","tag-rcs","tag-rcs-api","tag-rcs-bot","tag-rcs-business-messaging","tag-rcs-delivery-report","tag-rcs-messaging","tag-rcs-template","tag-rich-communication-services","tag-sms-api","tag-sms-gateway-center"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2958","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/users\/118"}],"replies":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/comments?post=2958"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2958\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/2959"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=2958"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=2958"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=2958"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}