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: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
Send a Carousel
Carousels render 2–10 cards viarich_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 isrich_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 areply 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 deliverspostback_datato 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 opendeep_linkin-app; every other recipient degrades to a plainopen_urlonfallback_url. Both URLs are required; omit the action entirely if you cannot supply an HTTPSfallback_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 underrich_card.card_content.suggestions(on the card) and/orrich_card.suggestions(message-level, below the card).rich_card_carousel— per-card chips under eachcard_contents[i].suggestions, plus optional message-levelrich_card_carousel.suggestions.
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.
Carousel constraints
- 2–10 cards per
rich_card_carousel.card_contents. A single-card carousel renders as malformed on Android — sendrich_cardinstead. card_widthisSMALLorMEDIUM(defaultMEDIUM). 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-levelrich_card_carousel.suggestionsrow 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.
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.
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.
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 isfalse, the payload also carriesfallback: "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.0is 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 arich_link_actionas a real UP3.0 rich deep-link. Whenfalse, the action’sfallback_urlis what the recipient opens — which is why the template and send contracts both require it.
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 omnichannelchannels 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:- 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_LIMITsends perDEVOTEL_RCS_RATE_WINDOW_MSwindow (defaults50per60000ms), 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. - 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.
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_dataholds the rich-card or carousel definition, including thenamethat 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/fallbackUrlpair is rejected with a422before the template reaches the RCS platform. - A template starts as
pendingand must be approved before a campaign can send it.PUTre-submits the definition and resets approval topending, so publish template updates before a campaign depends on them. - The list endpoint shows a top-level
sync_warningwhen 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
{ products, count } — an empty products array when no catalog
is connected. Use it to populate a product picker before sending.
Send a catalog carousel
- 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 openscheckout_url(HTTPS, label overridable withcheckout_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_idsto 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
202with 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:
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
503when 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:POST /api/v1/rcs/test-account/send-otpwith{ 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.- Poll
POST /api/v1/rcs/test-account/verify-otpwith{ verification_id }until the invite is accepted on the handset. While it is pending the endpoint answers425 PENDING_ACCEPTANCE— keep polling (the dashboard polls every ~4 seconds for up to 10 minutes). A declined invite returns422 INVITE_DECLINED; an unknown or expired session returns422. Start over from step 1 in either case. GET /api/v1/rcs/test-account/statusshows the demo boundary:{ verified, verified_phone, daily_sends, total_sends, suspended, rcs_capable, daily_remaining, lifetime_remaining, demo_messages, shared_bot_id }.
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-otpandchange-numberare capped at 5 calls per hour per organization; theverify-otppoll is capped at 30 per minute to match the poll cadence. Exceeding either returns429 RATE_LIMITEDwithretry_afterin 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 returnsnot_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, undersettings.cross_channel_fallback. Enable it once from the dashboard:
- Open Settings → Channels → Cross-Channel Fallback.
- 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). - 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_capablefor the recipient’s number. The RCS attempt short-circuits withRCS_NOT_SUPPORTEDand 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.
/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 ofDEVOTEL_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
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: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
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 isPOST /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.