RCS Business Messaging

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written on 2026-10-01 against the current Google documentation for RCS for Business (the product the older material calls RCS Business Messaging or RBM), with the LangChain 1.4.3 core and FastAPI 0.142.2 on Python 3.12 and the Spring AI 2.0.1 core on Spring Boot 4.1.1 with Java 21 (see the version baseline). RCS could not be validated for this page: access to the Developer Console requires an approved partner account and a launched, brand-verified agent, and no such account was used. The page was documented from official docs only. The Google guides were read on 2026-10-01 for the send endpoint, the message and suggestion limits, the webhook handshake and the signature algorithm; the exact JSON field names of incoming events and the OAuth scope are quoted from memory of the reference and must be checked against the REST reference before use. The snippets are illustrative, were written from the official documentation and are not compiled or run by this repository.

RCS (Rich Communication Services) is the carrier-backed successor of SMS: the default messaging app on the phone shows branded, verified conversations with images, carousels and tappable buttons, and delivery and read receipts are reported back to the sender. Unlike Slack or Telegram, there is no self-service bot token. A business reaches users only through a registered partner, and an agent is allowed to message users on a carrier’s network only after the carrier has approved its launch. The channel is therefore mostly a process problem; the code is the small part. The channel-agnostic core of Channel-Agnostic Architecture is unchanged.

Concepts

Term Meaning

Agent

The branded sender users see: name, logo, colour, description and contact details, with a verified badge once the brand is verified. Each agent has an agent ID used in API calls.

Partner

The registered organisation that owns agents and operates the integration: a solution provider, a carrier or another category chosen at registration. Partner display names appear in the billing reports carriers receive.

Carrier launch

Per-carrier, per-country approval for an agent to message that carrier’s subscribers. Until it is granted the agent can only message the phones of registered testers.

RBM API

The HTTPS and JSON API used to send agent messages and events and to check whether a phone can receive RCS.

Webhook

An HTTPS URL, configured per partner or per agent, to which Google posts user messages and delivery events.

Message traffic type

A label on each message (authentication, transaction, promotion, service request or acknowledgement) that drives policy checks and billing.

Onboarding and carrier launch

The Google guides describe the order of work, and it is worth planning around because the carrier steps take calendar time rather than engineering time.

flowchart LR A["Partner registration
interest form"] --> B["Developer Console
access"] B --> C["Create agent
and webhook"] C --> D["Test with
registered tester phones"] D --> E["Brand verification"] E --> F["Launch request
per carrier and country"] F --> G["Carrier approval
agent live"] G --> H["Production traffic"]
  1. Register as a partner. Complete the partner registration interest form. It asks for a corporate Google Account (a Gmail address is not accepted), company details, a service category, countries of operation and a technical contact. After approval you receive access to the RCS for Business Developer Console and to the RBM and Management APIs.

  2. Create the agent in the console: display name, logo, hero image, brand colour, privacy policy and terms links, contacts and the use case. Configure the webhook at this point (next sections).

  3. Test by adding tester phone numbers; testers receive an invitation and, once accepted, the agent can message them before any launch.

  4. Verify the brand and request launch with each carrier in each country you target. Carriers review the use case and message samples, so an AI assistant should be described honestly as an automated agent. The exact review criteria and timelines are carrier-specific and are not published as a fixed schedule; ask the partner contact.

  5. Operate. If a phone cannot receive RCS, the capability check or the send call returns a not-found style error and the documentation recommends falling back to another channel such as SMS.

A practical consequence for DocsAssistant: build the core, the webhook receiver and the SMS or web fallback first, and treat the carrier launch as the long pole that runs in parallel. Many teams also reach RCS through an aggregator that is already a registered partner instead of registering themselves; the API shapes are the same, only the credentials and onboarding contacts differ.

Webhook: handshake, signature and queueing

Google posts events to your HTTPS webhook (the URL must start with https://). Setup is two things in the console: the URL and a clientToken that only you and Google know. Three rules from the guide shape the whole receiver.

  1. Handshake. When the webhook is saved, Google sends a POST that carries a secret and your clientToken; the endpoint must answer 200 OK with the plain-text secret as the body.

  2. Signature. Every event carries an X-Goog-Signature header. To verify it, base64-decode the message.data field of the posted JSON, compute an HMAC with SHA-512 using the clientToken as the key over those decoded bytes, base64-encode the digest and compare it with the header in constant time.

  3. Be fast. Acknowledge with 200 OK within about five seconds and do the real work in the background. Failures are retried with exponential backoff (up to one attempt every ten minutes, for seven days), and one failing agent webhook can block delivery for the other agents of the same partner. A slow LLM call inside the request handler is therefore the wrong design; the model has to run behind a queue.

The posted body is an envelope whose message.data holds the base64 text of the event JSON. Per the reference, a user message event carries the sender phone number, a message ID, a send time, the agent ID and either free text or a suggestionResponse with the postbackData of a tapped suggestion; other events report delivery, read receipts, typing and the user starting a conversation. Treat those field names as something to confirm in the reference for your API version.

Python receiver with the LangChain core

import base64, hashlib, hmac, json, os
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
from fastapi.responses import PlainTextResponse

CLIENT_TOKEN = os.environ["RBM_CLIENT_TOKEN"].encode()   # placeholder: set in the secret store
app = FastAPI()
seen: set[str] = set()                                    # replace with Redis SET NX and a TTL


def valid(header: str, data_b64: str) -> bool:
    digest = hmac.new(CLIENT_TOKEN, base64.b64decode(data_b64), hashlib.sha512).digest()
    return hmac.compare_digest(base64.b64encode(digest).decode(), header)


@app.post("/rbm/webhook")
async def webhook(request: Request, background: BackgroundTasks):
    body = await request.json()
    if "secret" in body and body.get("clientToken") == CLIENT_TOKEN.decode():
        return PlainTextResponse(body["secret"])           # webhook verification handshake
    data = body["message"]["data"]
    if not valid(request.headers.get("X-Goog-Signature", ""), data):
        raise HTTPException(status_code=401)
    event = json.loads(base64.b64decode(data))
    mid = event.get("messageId")
    if mid and mid not in seen:                            # redeliveries are possible
        seen.add(mid)
        background.add_task(answer, event)
    return PlainTextResponse("ok")                         # acknowledge quickly

answer is the adapter proper. It maps senderPhoneNumber to userId (the phone number is personal data, so hash or pseudonymise it before it reaches logs and traces, see RAG Access Control & Privacy) and builds the conversationId from the agent and phone. The phone number is the identity: there is no thread concept as in Slack, so one conversation per phone is the natural model, with the memory window managed by the core (LangChain: Short-Term Memory & Checkpointers).

import asyncio, httpx, uuid
from google.oauth2 import service_account
from google.auth.transport.requests import Request as AuthRequest

SCOPES = ["https://www.googleapis.com/auth/rcsbusinessmessaging"]   # confirm in the REST reference
creds = service_account.Credentials.from_service_account_file(
    os.environ["RBM_SERVICE_ACCOUNT_FILE"], scopes=SCOPES)
AGENT_ID = os.environ["RBM_AGENT_ID"]
BASE = "https://rcsbusinessmessaging.googleapis.com/v1"             # regional hosts also exist
http = httpx.AsyncClient(timeout=10)


async def send(phone: str, content: dict) -> None:
    if not creds.valid:
        await asyncio.to_thread(creds.refresh, AuthRequest())        # blocking call, keep it off the event loop
    response = await http.post(
        f"{BASE}/phones/{phone}/agentMessages",
        params={"messageId": uuid.uuid4().hex, "agentId": AGENT_ID},
        headers={"Authorization": f"Bearer {creds.token}"},
        json={"contentMessage": content, "messageTrafficType": "SERVICEREQUEST"},
    )
    response.raise_for_status()


async def answer(event: dict) -> None:
    phone = event["senderPhoneNumber"]
    text = event.get("text") or event.get("suggestionResponse", {}).get("postbackData", "")
    result = await core_ask(conversation_id=f"rcs:{AGENT_ID}:{phone}", user_id=pseudonym(phone), text=text)
    for content in to_rbm_content(result):   # whole messages, in order: RCS has no token streaming
        await send(phone, content)

core_ask and pseudonym are the shared core call and a keyed hash; to_rbm_content is shown below and returns the list of messages to send. The token refresh is blocking, so it runs in a worker thread, and the POST uses an async client so neither stalls the event loop. The send endpoint and its query parameters (messageId, agentId) are as documented; use the regional endpoint the documentation lists for your data region.

Java receiver with the Spring AI core

@RestController
class RbmWebhook {
  private final byte[] clientToken = System.getenv("RBM_CLIENT_TOKEN").getBytes(StandardCharsets.UTF_8);
  private final ObjectMapper mapper = new ObjectMapper();
  private final RbmWorker worker;                       // queue-backed, wraps the Spring AI core

  RbmWebhook(RbmWorker worker) { this.worker = worker; }

  @PostMapping("/rbm/webhook")
  ResponseEntity<String> receive(@RequestBody JsonNode body,
      @RequestHeader(value = "X-Goog-Signature", required = false) String signature) throws Exception {
    if (body.has("secret") && body.path("clientToken").asText().equals(new String(clientToken, UTF_8))) {
      return ResponseEntity.ok(body.get("secret").asText());       // handshake
    }
    byte[] payload = Base64.getDecoder().decode(body.path("message").path("data").asText());
    Mac mac = Mac.getInstance("HmacSHA512");
    mac.init(new SecretKeySpec(clientToken, "HmacSHA512"));
    byte[] expected = Base64.getEncoder().encode(mac.doFinal(payload));
    if (signature == null || !MessageDigest.isEqual(expected, signature.getBytes(UTF_8))) {
      return ResponseEntity.status(401).build();
    }
    worker.submit(mapper.readTree(payload));            // returns immediately; ChatClient runs on the worker
    return ResponseEntity.ok("ok");
  }
}

The worker calls the same ChatClient pipeline described in Spring AI: ChatModel & ChatClient, with chat memory keyed by the conversationId (Spring AI: Chat Memory). Sending is an authenticated HTTPS POST to the same agentMessages endpoint; use Google’s API client or google-auth-library for the service account token.

Rich cards, carousels and suggestions

RCS has no token streaming, so the adapter waits for the whole answer, optionally sends a typing event while it works, and renders the result as whole messages: the text, then rich content. The limits from the send guide (read on 2026-10-01):

Element Limit

Text message

Up to 3 072 characters; messages of 160 UTF-8 characters or fewer are billed as "basic messages" by the documentation’s rules, so short answers are cheaper.

Whole message

250 KB when serialised.

Rich card

Media, title, description and up to 4 persistent suggestions; media height SHORT, MEDIUM or TALL; vertical or horizontal layout.

Carousel

2 to 10 rich cards with a small, medium or large card width.

Suggestions

At most 11 per message (up to 4 persistent and 7 transient). A suggested reply carries display text and a postbackData string; a suggested action opens a URL, dials, shows or shares a location or creates a calendar event.

Media

Images, video and audio in common formats, up to 100 MiB per file.

For DocsAssistant, a good mapping is: the answer as text; each citation as a rich card (a standalone card for one citation, a carousel for two to ten) with the page title, a short quote and an "Open" URL action; and follow-up questions as suggested replies whose postbackData is the question the user would otherwise type. Because the postback returns through the webhook as a normal event, the same answer function handles typed and tapped input.

def to_rbm_content(result) -> list[dict]:
    follow_ups = [{"reply": {"text": q[:25], "postbackData": q}} for q in result.follow_ups[:3]]
    if not result.citations:
        return [{"text": result.answer[:3072], "suggestions": follow_ups}]
    cards = [{
        "title": c.title[:200],
        "description": c.quote[:2000],
        "suggestions": [{"action": {"text": "Open", "postbackData": f"open:{c.id}",
                                    "openUrlAction": {"url": c.url}}}],
    } for c in result.citations[:10]]
    if len(cards) == 1:                      # a carousel needs 2 to 10 cards
        rich = {"standaloneCard": {"cardOrientation": "VERTICAL", "cardContent": cards[0]}}
    else:
        rich = {"carouselCard": {"cardWidth": "MEDIUM", "cardContents": cards}}
    return [{"text": result.answer[:3072]},                     # the answer first ...
            {"richCard": rich, "suggestions": follow_ups}]      # ... then the citations and follow-ups

The field names above follow the message reference as remembered at writing time (a single card is a standaloneCard with one cardContent, a carousel is a carouselCard of cardContents, and suggestions are reply or action objects); the 25-character reply label and the string lengths here are conservative choices, not the documented limits, so check them in the REST reference. With citations, the function returns two messages, the answer as text followed by the card or carousel carrying the follow-up suggestions, rather than one oversized message, and answer sends them in that order. The citation mechanics themselves are in Citations & Grounding.

Fallback and reach: Android, iOS and SMS

RCS for Business is documented by Google as reaching users on Android and iOS. On Android the Google Messages app is the client. Support on iPhone comes through the carriers rather than through an Apple business API: Apple added RCS to Messages in iOS 18, and business messaging on iPhone depends on the carrier having enabled it for its subscribers, so reach varies by country and carrier. Secondary sources report that iOS 18.1 started a staged rollout of business RCS with selected operators and that the large US carriers and many virtual operators are enabled; those statements were not found on a Google or Apple page for this article, so verify current coverage with your partner before committing to a launch plan.

Design for the case where the phone cannot receive RCS, which the documentation says is signalled by a not-found response: keep the same answer text, drop the cards and send it by SMS (or by email or the web widget) through your provider. Features that differ from the other business channels in this sub-section:

Aspect RCS Business Messaging Compare

Identity

Phone number (MSISDN), verified by the carrier network.

Teams and Slack give an organisation-scoped ID; Apple Messages for Business gives an opaque per-business ID.

Initiation

Agents may start conversations, subject to message traffic type and carrier policy.

WhatsApp Business restricts business-initiated messages to templates outside the 24-hour window.

Streaming

None; whole messages with optional typing events.

Slack and Telegram have native streaming.

Approval

Partner registration, brand verification, per-carrier launch.

Apple Messages for Business also needs a provider and Apple approval.

Production notes

  • Idempotency. Keep the processed messageId set in Redis with a TTL; the retry schedule above means duplicates occur after any outage of your webhook.

  • Queueing. Because a failing webhook can stall the partner queue, store events first (a durable queue or a table) and answer 200 OK before calling the LLM. See Operating Chat Channels for retries, observability and cost monitoring shared by all channels.

  • Privacy. The phone number is the user’s identity and personal data. Pseudonymise it for the core, and apply the retention rules of your agreement with the partner.

  • Opt-out. Honour stop requests and the traffic-type rules of the carrier; the assistant should not initiate conversations unless the use case approved at launch covers it.

  • Secrets. The clientToken and the service account key belong in a secret store (Azure Key Vault & Secrets is one option), never in the repository.