Skip to main content

RCS

Deliver app-like messaging experiences natively in users’ default messaging apps with RCS Business Messaging. Orbit provides RCS via DotGo’s RBM Hub (the same RCS aggregator powering bot registration, template approval, and carrier delivery throughout Orbit), supporting rich cards, carousels, suggested actions, and branded sender profiles. This page is the concept-and-reference surface: message shapes (rich cards, carousels, suggestion chips), the capability check, fallback, rate limits, templates, commerce, and reach. For the ordered walkthrough from first brand registration to the first launched send, follow the RCS onboarding guide.

Send an RCS Message

The same plain-text send with suggestion chips in four languages:
A send is either plain text (body), a standalone rich_card, or a rich_card_carousel — they are mutually exclusive. For text and media sends, attach chips under the top-level suggested_replies array; rich cards and carousels carry their own chips under rich_card.suggestions and rich_card_carousel.suggestions. Each suggestion chip is either a reply ({ text, postback_data }) or an action ({ text, open_url | dial_action | view_location_action }).

Send a Rich Card

Carousels render 2–10 cards via rich_card_carousel.card_contents. A single-card carousel renders as malformed on Android — use rich_card for one card.

Features

  • Rich cards — image, title, description, and action buttons in a single message
  • Carousels — horizontally scrollable card collections (2–10 cards)
  • Suggested replies & actions — tap-to-reply buttons and deep links
  • Branded sender — verified business name, logo, and color in the chat thread
  • Typing indicators — real-time composing indicators
  • Read receipts — delivery and read confirmation
  • SMS fallback — automatic fallback to SMS when RCS is unavailable

Rich card specification

A rich card is rich_card.card_content — a title (1–200 chars), a description (1–2000 chars), an optional media, and optional suggestions.

Media tiers

rich_card.card_content.media carries a height tier and a content_info block: On a horizontal card you can also set rich_card.thumbnail_image_alignment (LEFT or RIGHT); it is ignored on vertical cards because the RBM spec rejects it there.

Suggestion chips

Each chip is either a reply or an action — quoted here exactly as the API validates it, because the two are mutually exclusive inside one chip:
  • reply{ text, postback_data }. Tapping sends the text back as a user message and delivers postback_data to your webhook.
  • action{ text, postback_data?, … } where the tail is one of:
    • open_url: { url } — open an HTTPS link.
    • dial_action: { phone_number } — start a call.
    • view_location_action: { label?, query?, lat_long? } — open a map.
    • rich_link_action: { deep_link, fallback_url, app_link?, tracking_params? } — a GSMA Universal Profile 3.0 rich deep-link. UP3.0-capable handsets open deep_link in-app; every other recipient degrades to a plain open_url on fallback_url. Both URLs are required; omit the action entirely if you cannot supply an HTTPS fallback_url.
text is capped at 25 characters; postback_data at 2048. Up to 11 chips per send.

Where chips live — and the exclusion rule

Chip placement depends on the message shape, and the placements are mutually exclusive with the top-level field:
  • Plain text or media send — chips under top-level suggested_replies.
  • rich_card — chips under rich_card.card_content.suggestions (on the card) and/or rich_card.suggestions (message-level, below the card).
  • rich_card_carousel — per-card chips under each card_contents[i].suggestions, plus optional message-level rich_card_carousel.suggestions.
A send that combines rich_card or rich_card_carousel with top-level suggested_replies is rejected — the chips belong inside the card payload, not beside it. In short: the suggestion-chip union (reply vs action) is mutually exclusive with the top-level suggested_replies array whenever a rich payload is present.
  • 2–10 cards per rich_card_carousel.card_contents. A single-card carousel renders as malformed on Android — send rich_card instead.
  • card_width is SMALL or MEDIUM (default MEDIUM). Keep the width consistent across the carousel; mixing widths is not supported.
  • Each card carries its own suggestions — per-card chips (e.g. a “Buy” on each product) — plus an optional message-level rich_card_carousel.suggestions row rendered below the carousel.
  • Keep per-card text short: titles render truncated past ~2 lines on narrow screens, and media heights should match across cards so the carousel does not jump vertically while swiping.
Fallback when a card cannot render — if a recipient handset refuses the rich payload (for example the carrier downgraded the message), the send is logged with the RCS-specific failure and the configured cross-channel fallback chain advances — or, without a chain, the send fails closed with a delivery receipt you can read on the message status. There is no silent partial render.

Onboarding: brand → agent → verify → launch

RCS has the longest onboarding path of any channel — four ordered gates, each with a pollable signal. Gate 1 involves no waiting: a brand can back the agent as soon as it is submitted, because the brand and the agent registration travel to the carrier gateway in one combined call — the only states that block it are a refusal (rejected) or a takedown (suspended). The full walkthrough (with every curl, the 422 handling, and the status tables) lives in the RCS onboarding guide; the checklist below is the same four gates compressed to the decisions. The brand steps above (steps 1–5) cover gates 1 and 2 in full.

Gate 3: verification multipart submission

POST /api/v1/rcs/bots/:id/verify accepts multipart/form-data with the evidence carriers review:
  • screenImages[] — up to 10 conversation screenshots showing the agent in use.
  • kycdocs[] — up to 5 KYC documents (business registration, tax record, and so on).
  • brandLogoImage — one logo file; keep it small (≈90 KB recommended).
  • data — an optional JSON field that overrides values otherwise pulled from the linked brand.
Files are capped at 5 MB each, 16 files per request. A JSON-only body carrying just data is accepted, but carriers normally refuse a submission with no screenshots, and a bot whose brand lacks the underlying KYC fields returns 409 because the submission cannot be built. On success the bot moves to pending_verification.

Gate 4: launch — and the 409 on an early call

POST /api/v1/rcs/bots/:id/launch asks the carriers to put a verified bot live. Calling it before gate 3 completes returns 409 INVALID_LAUNCH_STATE — poll the bot until verification_status is verified before attempting launch. Two optional fields shape the rollout:
  • carrier_mccmnc — a list of MCCMNC ids to stage the launch across a subset of networks instead of all carriers at once.
  • comment — up to 2000 characters the carriers see with the request.
The call moves the bot to pending_launch immediately; each carrier’s decision then lands asynchronously on the bot’s carrier_statuses, so poll GET /api/v1/rcs/bots/:id/quality for the per-carrier map rather than expecting a launched bot back from this call.

Capability check

Before spending a send on a recipient, probe the pair:
botId is the bot’s provider id from the bot list (dotgoBotId), and the recipient is E.164. The response interprets as:
  • isCapable — the handset can receive RCS from this bot right now. When it is false, the payload also carries fallback: "sms" — honor it and route the message down the SMS path in the same decision instead of spending an RCS send that will fail closed.
  • features — the raw RBM capability tokens the handset advertised.
  • universalProfileVersion — the negotiated GSMA Universal Profile version. 3.0 is the floor for rich deep-links and MLS encryption.
  • featureFlags.mlsSupported — the recipient accepts MLS-encrypted RCS; relevant if you rely on encrypted traffic.
  • featureFlags.richLinkSupported — the recipient renders a rich_link_action as a real UP3.0 rich deep-link. When false, the action’s fallback_url is what the recipient opens — which is why the template and send contracts both require it.
Positive verdicts are cached for 24 hours, so a bulk pre-send sweep is cheap; uncached misses cost one Dotgo probe each. Orbit also runs the same check inline on every send and short-circuits with RCS_NOT_SUPPORTED when the recipient is not capable — calling it yourself is for pre-campaign planning (and the reach scan), not a send-path requirement.

Fallback chain

RCS → SMS fallback is org-level, not per-request: the request body for /messages/rcs accepts no fallback field (one is ignored), and the chain lives on the organization under settings.cross_channel_fallback. See Configure RCS → SMS fallback above for the dashboard steps. On a capability-check failure the chain behaves exactly like a manual fallback: "sms" decision: the RCS attempt short-circuits with RCS_NOT_SUPPORTED, and the router hands the same message body to the next channel in the chain — with one guard: when the chain falls through to SMS, Orbit re-runs the SMS opt-out and DNC check against the recipient, so a contact who opted out of SMS is suppressed rather than re-contacted there. Without a chain, a not-capable send fails closed with a RCS_NOT_SUPPORTED delivery receipt.

RCS in omnichannel campaigns

The Fallback chain above is a send-time mechanism — it applies to any RCS send, one message at a time. For a campaign, Orbit adds a second layer: the campaign’s omnichannel channels array is an ordered ladder (primary at position 0, each subsequent hop a fallback) declared on the campaign itself, so the dispatch path can walk RCS → WhatsApp → SMS (for example) per recipient instead of relying on the organization-wide chain alone. Author the ladder in the campaign wizard’s Setup step (the “Channel chain” panel) or via the API: channels on POST /campaigns — validated by campaignChannelChainSchema so each hop carries its fallback_on trigger (failed / no_delivery / no_engagement) and fallback_after_seconds window; the last entry is terminal and carries neither. The chain’s FIRST entry must match the campaign’s top-level channel. RCS hops work because every RCS send carries the same capability probe → RCS_NOT_SUPPORTED signal the org-level chain consumes — the dispatch layer then advances to the next entry instead of failing the recipient. Two preconditions are worth stating because only the first attempt may be an RCS send: (1) only a verified or launched agent can send, so the ladder’s first hop is valid only after you complete Register a verified agent; (2) the SMS hop in the ladder uses your normal SMS sender and opt-out screening — no special configuration is needed beyond an available SMS number.

Rate limits

Two layers cap RCS traffic:
  1. Outbound send throttle (per tenant). Orbit throttles calls to Dotgo’s RBM Hub with a Redis-backed sliding-window limiter scoped per tenant — each account gets its own ceiling of DEVOTEL_RCS_RATE_LIMIT sends per DEVOTEL_RCS_RATE_WINDOW_MS window (defaults 50 per 60000 ms), so one tenant’s volume never eats another’s. Both knobs are operator-tunable without a redeploy; size against your contracted Dotgo TPS tier. See guides/rate-limits for the platform-wide view.
  2. API route limits. Every /api/v1/rcs/* route sits under the standard authenticated read/write API limits. The test-account verification poll has its own per-minute budget sized to the dashboard’s poll cadence — see Test account below.
A 429 from the send path carries retry_after in seconds; queue and retry after that interval rather than hammering the window.

Templates

Templates let you register a rich card or carousel once and reference it from campaigns by name. In the dashboard, the RCS Templates tab shows the same roster with per-template approval badges and inline deliverability. The REST lifecycle:
  • rich_template_data holds the rich-card or carousel definition, including the name that identifies the template on later reads, updates, and sends.
  • Any rich deep-link action inside the definition is validated on submission: a missing or malformed deepLink / fallbackUrl pair is rejected with a 422 before the template reaches the RCS platform.
  • A template starts as pending and must be approved before a campaign can send it. PUT re-submits the definition and resets approval to pending, so publish template updates before a campaign depends on them.
  • The list endpoint shows a top-level sync_warning when the background approval-status refresh could not run — the templates are still returned; the statuses may be one refresh behind.

Template analytics

GET /api/v1/rcs/analytics/templates?days=30 returns one row per template_name with sent, delivered, read, clicked and the derived delivery_rate / read_rate / click_rate (0–100, read and click rates measured against delivered), most-sent first. The days window is 1–90 (default 30). Messages sent without a template group under (no template), and a window with no RCS traffic returns an empty array.

Commerce

RCS commerce reuses the same product catalog as the WhatsApp surface — connect a catalog once in Commerce Manager and it is sendable on both channels.

List the catalog

Returns { products, count } — an empty products array when no catalog is connected. Use it to populate a product picker before sending.
  • Products render as a rich-card carousel with two suggested-action layers: a per-card add-to-cart chip (label overridable with add_to_cart_label) and a message-level checkout chip that opens checkout_url (HTTPS, label overridable with checkout_label).
  • At least two usable products are required — fewer returns a validation error (the same single-card-carousel constraint as the raw payload). Omit product_ids to send the whole catalog; the renderer caps it at the RBM carousel ceiling of 10 cards.
  • The send runs through the standard message pipeline — billing, delivery receipts, scheduling, and the per-tenant send throttle all apply unchanged. The response is 202 with the queued message; delivery is asynchronous.

Reach scan

POST /api/v1/rcs/reach-scan estimates how much of a contact segment can actually receive RCS from a bot before you build the campaign:
It samples up to sample_size contacts (default 50) from the segment, probes each number’s RBM capability, and returns { segment_id, bot_id, sampled, capable, reach_percent, recommended_channel } where recommended_channel is rcs, mixed, or sms. Use it to decide between an RCS-only send and an SMS fallback split. Note it fans out billable capability lookups and reads contact phone numbers, so it sits behind the owner/admin/developer role guard. An unknown bot or segment returns 404 rather than a misleading 0% reach.

Reach and quality after launch

GET /api/v1/rcs/bots/:id/quality reports one bot’s sender health: status, verification_status, whether it is launched on at least one carrier, and the per-carrier carrier_statuses map with carriers_launched out of carriers_total. A bot launched on only some carriers cannot reach subscribers on the rest — check this before a campaign and pair it with a reach scan on the target segment. Per-day volume (sent / delivered / read) comes from GET /api/v1/rcs/analytics/daily?days=30 (window 1–90; days with no traffic are omitted rather than zeroed).

Tester devices

A bot can exchange messages with tester devices while it is still unverified or unlaunched — this is how you preview rich cards, carousels, and suggested actions on real hardware before carriers approve anything.
  • Register your own device first — an unlaunched bot cannot message any other number.
  • The tester-invite endpoint delivers the invite onto the handset; once the recipient taps accept, the bot can message that number while still unlaunched. A failed invite returns a retryable 503 when the upstream is briefly unavailable — retry it.
  • Removing a tester stops an unlaunched bot from reaching it immediately; a launched bot still reaches the number as an ordinary subscriber.

Test account

Before you have any brand at all, the test account lets you try RCS on Devotel’s shared bot against one verified handset. The flow:
  1. POST /api/v1/rcs/test-account/send-otp with { phone } — checks the handset’s RCS capability, registers it as a tester device on the shared bot, and delivers the tester invite. Returns { verification_id, … }. A handset with no RCS support is rejected rather than registered.
  2. Poll POST /api/v1/rcs/test-account/verify-otp with { verification_id } until the invite is accepted on the handset. While it is pending the endpoint answers 425 PENDING_ACCEPTANCE — keep polling (the dashboard polls every ~4 seconds for up to 10 minutes). A declined invite returns 422 INVITE_DECLINED; an unknown or expired session returns 422. Start over from step 1 in either case.
  3. GET /api/v1/rcs/test-account/status shows the demo boundary: { verified, verified_phone, daily_sends, total_sends, suspended, rcs_capable, daily_remaining, lifetime_remaining, demo_messages, shared_bot_id }.
Demo boundaries and the 429 behavior:
  • 20 demo sends per day, 200 per account lifetime. Re-verifying a new handset does not reset the quota — the counters deliberately survive POST /api/v1/rcs/test-account/change-number.
  • send-otp and change-number are capped at 5 calls per hour per organization; the verify-otp poll is capped at 30 per minute to match the poll cadence. Exceeding either returns 429 RATE_LIMITED with retry_after in seconds — wait it out; the verify-otp window is per-minute so a 429 there clears in under a minute.
  • The test account is a demo surface: production sends need your own approved brand and launched bot (the onboarding gates above).

RCS Availability

RCS is supported on Android devices with Google Messages. When the recipient’s device cannot receive RCS (Apple device, older Android, RBM not enrolled, or the carrier does not bridge to the RBM hub), Orbit can automatically retry the send over SMS without changing your request. The fallback chain is configured per organization, not per request. Once configured, every RCS send transparently retries the next channel in the chain when delivery fails for an RCS-specific reason (capability check returns not_capable, all RCS providers exhaust their attempts, or the recipient has not opted in to RCS in their messaging app).

Configure RCS → SMS fallback

The cross-channel fallback chain lives on the organization, under settings.cross_channel_fallback. Enable it once from the dashboard:
  1. Open Settings → Channels → Cross-Channel Fallback.
  2. Add a rule with Primary: RCS and Fallback: SMS (you can chain more channels — e.g. RCS → WhatsApp → SMS — and they will be tried in order).
  3. Save. The change takes effect on the next send; no redeploy or send-path change is required.

What triggers the fallback

  • Capability check fails — Orbit’s capability cache (or a live Dotgo probe) reports not_capable for the recipient’s number. The RCS attempt short-circuits with RCS_NOT_SUPPORTED and the router advances to the next channel in the chain.
  • All RCS providers fail — every registered RCS provider returns a non-retryable error within the configured retry budget.
  • TCPA opt-out re-check — when the chain falls through to SMS, Orbit re-runs the SMS opt-out + DNC check against the recipient. If they have opted out of SMS (even though they accepted RCS), the message is suppressed rather than delivered over SMS.
The request body for /messages/rcs does not accept a fallback field — passing one is ignored. Fallback behavior is entirely driven by the organization-level configuration so it stays consistent across every send path (API, campaigns, journeys, inbox replies) without requiring callers to remember to set a flag.

Rate limits

Orbit throttles outbound calls to Dotgo’s RBM Hub with a Redis-backed sliding-window limiter so you stay within Dotgo’s agreed throughput and avoid upstream 429s. The limit is scoped per tenant: each account gets its own ceiling of DEVOTEL_RCS_RATE_LIMIT sends per DEVOTEL_RCS_RATE_WINDOW_MS window, so one tenant’s send volume never eats into another’s. Size your capacity planning against this per-tenant ceiling, not a shared global pool. Both knobs are operator-tunable without a redeploy — set the env var (Secret Manager / cluster env) and restart the pod: Raise DEVOTEL_RCS_RATE_LIMIT and DEVOTEL_RCS_RATE_WINDOW_MS together so the effective per-tenant rate (sends ÷ window) matches your Dotgo TPS tier — bumping one without the other changes the burst shape, not just the throughput.

Register a verified agent (brand)

Every RCS send goes out under a verified business agent. Before carriers launch your agent, they review the identity of the business behind it — the brand. You fill in the brand form once, submit it, and create the agent right away: the brand and the agent travel to the carrier gateway together in one combined registration, so nothing on this platform waits between the two steps. Only a brand that was explicitly refused or taken down blocks agent creation.

1. Fill in the brand details in the dashboard

Open Settings → Channels → RCS → Brands and create a brand. The form collects the fields carriers and the RBM aggregator review: brand name, website, logo URL (a public, HTTPS-hosted square image), industry vertical, a contact person with email and phone, and the registered business address. A tax ID and legal entity name are optional — most carriers skip them, but a few (for example in India and Brazil) ask for them before approving an agent, so enter them when you have them. Save the draft; nothing is sent for review until you submit in step 3.

2. Or do the same through the API

The response is 201 with the stored brand in draft status. Keep data.id — you pass it to the update, submit, and agent-create endpoints. Until you submit the brand, you can refine it with PATCH /api/v1/rcs/brands/:id. A 422 response lists exactly which fields failed validation (for example a logo URL that is not HTTPS, or a country that is not a two-letter code).

3. Submit the brand for review

When the draft is complete, submit it:
Submission moves the brand to pending_review — and that is the whole registration on your side. The brand details travel to the RBM aggregator together with your agent in a single combined registration call, and the carriers adjudicate the brand and agent as one unit — typical end-to-end timing is 3–5 business days, and carriers are the long pole. There is no screening step on Orbit’s side: you can move straight to step 5 and create the agent as soon as the brand is submitted. In the rare case the brand is refused for cause it flips to rejected — fix the named fields and re-submit.

4. Check the status — for visibility, not to gate the agent

The status field moves through: A rejected brand goes back to pending_review when you fix the flagged fields and submit again — you do not have to start from a new draft.

5. Create the agent on the brand

Create the agent (bot) attached to the brand as soon as the brand is submitted, attach your test devices, and let carriers verify it. In the dashboard this is Settings → Channels → RCS → Agents; over the API it is POST /api/v1/rcs/bots with brand_id set to the submitted brand. The brand and the agent go to the carrier gateway in one combined registration, so no approval has to land first — only a brand that was refused (rejected) or taken down (suspended) returns 409 here. Later agents reuse the same registered brand. The first RCS send after launch renders under your verified business name, logo, and brand color — which is also the deliverability signal subscribers look for.

During the wait: fallback and rate limits

Neither the fallback chain nor the rate limits changes while a brand is in review — they are org-level settings and apply as soon as the agent is live. If you expect recipients on non-RCS devices, wire the RCS → SMS fallback before launch so the first send cannot strand.