A typical send is an HTTPS POST with a JSON body describing the message. Following Google RBM conventions, a minimal text send looks like this (endpoint and IDs simplified):
POST https://{region}-rcsbusinessmessaging.googleapis.com/v1/phones/{E164_PHONE}/agentMessages?agentId={AGENT_ID}
Content-Type: application/json
{
"contentMessage": {
"text": "Your order #1043 has shipped 📦",
"suggestions": [
{ "action": { "text": "Track", "postbackData": "track_1043",
"openUrlAction": { "url": "https://shop.example/track/1043" } } }
]
}
}
Before sending, call the capability check for the destination; if it isn’t RCS-capable, send via SMS/MMS instead (a good provider does this fallback for you).
The three things every RCS API does
Strip away naming differences between providers and an RCS API does three jobs.
It answers "can this number receive RCS?" RCS reach depends on the handset, the carrier, and the user's own settings, and it changes over time, so capability is a question you ask per number at send time rather than a property you store. Everything else follows from the answer.
It sends a message from a verified agent. The message is addressed from your approved sender rather than from a number, which is what produces the brand name, logo, and checkmark on the recipient's phone. The payload describes what the message contains: text, a rich card, a carousel, and the suggested replies or actions attached to it.
It delivers what happened back to you. RCS is two-way and event-rich in a way SMS is not. Beyond delivery, you receive read receipts, free-form replies, and taps on individual suggestions, each identifiable, so you can tell which button a customer pressed rather than only that they responded.
Authentication and environments
Access is credentialed per environment, so the keys that drive a staging integration are not the keys that can message real customers. This matters more in messaging than in most APIs, because a mistake is not a bad database write you can roll back, it is a message that has already arrived on someone's phone. Treat a production messaging credential with the same care as a payment credential, and rotate it on the same schedule.
Sending through the SimplyRCS API
The SimplyRCS API is documented as an OpenAPI 3.1 spec you can read without an account: the machine-readable spec is public. Everything below comes from it. Authenticate with either an Authorization: Bearer token or an X-API-Key header; both are accepted and both take the same key.
A send is one POST. The only required field is the contact:
curl -X POST https://simplyrcs.signalmash.com/v1/messages/send \\
-H "Authorization: Bearer $SIMPLYRCS_API_KEY" \\
-H "Content-Type: application/json" \\
-H "Idempotency-Key: order-1043-shipped" \\
-d '{
"contactId": "ct_8f21",
"channelType": "RCS",
"messageType": "TRANSACTIONAL",
"fallbackToSms": true,
"content": {
"type": "text",
"text": "Your order #1043 has shipped.",
"actions": [{ "text": "Track", "url": "https://shop.example/track/1043" }]
}
}'
The same call in Node, with the idempotency key derived from the thing you are notifying about rather than randomly generated, which is what makes a retry safe:
const res = await fetch("https://simplyrcs.signalmash.com/v1/messages/send", {
method: "POST",
headers: {
"X-API-Key": process.env.SIMPLYRCS_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": `order-${order.id}-shipped`,
},
body: JSON.stringify({
contactId: order.contactId,
channelType: "RCS",
messageType: "TRANSACTIONAL",
fallbackToSms: true,
content: {
type: "text",
text: `Your order #${order.id} has shipped.`,
actions: [{ text: "Track", url: order.trackingUrl }],
},
}),
});
if (!res.ok) throw new Error(`send failed: ${res.status} ${await res.text()}`);
const { messageId, fallbackUsed } = await res.json();
The response tells you what actually happened to it, which matters because RCS is not guaranteed to be the channel that delivered:
{
"messageId": "...",
"conversationId": "...",
"status": "...",
"billingType": "...",
"providerMessageId": "...",
"fallbackUsed": false,
"fallbackChannelId": null
}
Three details in that call are easy to get wrong and expensive to get wrong.
messageType defaults to MARKETING. Omit it and the strictest consent gates apply, so a transactional receipt can be blocked for a contact who never opted into marketing. Set it explicitly on every send rather than relying on the default.
Idempotency-Key is how you survive a retry. Pass a printable-ASCII key up to 255 characters and a repeat of the same request replays the original response for 24 hours instead of sending a second message. A timeout on a messaging API is not like a timeout on a database write: without an idempotency key, the safe-looking retry is a duplicate message on a real phone.
Fallback is a field, not a fallback plan you build. Set fallbackToSms and read fallbackUsed on the response to know which channel carried it, since the two bill differently.
Errors come back shaped consistently. These are the live responses, not illustrations:
{ "error": "FST_ERR_VALIDATION", "message": "body must have required property 'contactId'" }
{ "error": "UNAUTHORIZED", "message": "Authorization header or X-API-Key header required" }
Two adjacent endpoints are worth knowing before you design the flow. GET /v1/contacts/{id}/rcs-check reports whether a contact can actually receive RCS, which is how you decide what to compose rather than discovering it at send time. POST /v1/messages/preview renders a message without sending it. There is also POST /v1/messages/{id}/revoke for pulling back a message that has not yet been delivered.
On rate limits, and what the spec does not say. Worth stating plainly, because the honest answer is more useful than a made-up number: the published spec documents no general rate limit for the messaging endpoints, and not one of its 143 paths declares a 429 response. The single documented limit sits on a public campaign-enrolment endpoint, at 10 requests per minute per IP, and does not describe the API as a whole.
Design as though a limit exists anyway. An undocumented ceiling is not the same as no ceiling, it is a ceiling you will discover in production. Handle non-2xx responses with backoff rather than an immediate retry loop, keep the idempotency key stable across those retries so a throttled call that actually succeeded cannot send twice, and confirm your expected throughput with support before a launch that depends on it.
Webhooks are registered through the API rather than a dashboard-only setting: POST /v1/webhook-endpoints takes a url and an events array, POST /v1/webhook-endpoints/{id}/test fires a test delivery, and GET /v1/webhook-endpoints/{id}/deliveries shows what was attempted and what happened. API keys are managed the same way, including POST /v1/api-keys/{id}/rotate, which mints a new secret while keeping the key id, name, scope, and expiry, and stops the old secret authenticating immediately.
Webhooks are where the real design work is
Most of the engineering effort in an RCS integration is inbound rather than outbound. Sending is a request; receiving is a system.
Three properties decide whether that system is sound. Signing lets you verify an event genuinely came from your provider rather than from anyone who found your endpoint, so the signature should be checked before the payload is trusted. Idempotency matters because delivery is at-least-once: the same event can legitimately arrive twice, and an integration that books two appointments from one reply is a bug in your handler rather than in the network. Keep the event identifier and discard repeats. Ordering and replay cover the rest, since your endpoint will eventually be down when something important happens, and the question is whether those events queue and redeliver or vanish.
Our own platform provides signed webhooks, idempotency keys, at-least-once delivery with deduplication, ordered conversation streams, event replay, and a dead-letter queue for events that never succeed. See the RCS webhook guide and RCS event types for what arrives and when.
Fallback is an API behaviour, not a feature you build
The single largest difference between RCS APIs is what happens when the recipient cannot receive RCS. If fallback is your responsibility, you are maintaining two integrations, two message formats, and your own logic for deciding between them, and you will get the edge cases wrong before your provider does.
Where fallback is handled for you, one send reaches the whole list: rich where possible, SMS or MMS everywhere else, with delivery reported per channel so you can see which messages landed as which. That per-channel reporting is worth asking about specifically, because a provider that only reports "delivered" cannot tell you what the rich format is actually earning you. See RCS fallback to SMS.
Throughput and what governs it
Sending limits in the US are not set by your provider's infrastructure but by your registration. Your brand and campaign records produce a trust score, and that score governs how much you may send per day. An integration that works in testing and throttles in production is usually hitting that ceiling rather than an API limit. See how 10DLC registration works, since the same registration also governs the SMS fallback leg.
MCP for AI agents
Alongside the REST interface, SimplyRCS exposes an MCP server, which lets an AI agent use messaging as a tool through standard tool calls rather than through bespoke integration code. That is a meaningfully different integration path from a conventional API: the agent discovers what it can do rather than being programmed against a fixed contract. Few providers offer it.
It lives at /v1/mcp on the same host as the REST API, with a Server-Sent Events transport at /v1/mcp/sse, and it authenticates with the same API key and the same two header forms as everything else. That last point matters more than it sounds: there is no separate credential system for the agent path, so an API key you have already scoped and can already rotate is the same key the agent uses.
The practical difference is what you write. Against the REST API you write the integration: which endpoint, which fields, what to do with the response. Against MCP you grant a capability and the agent works out the call. For a bot that needs to check whether a contact can receive RCS, send accordingly, and read the delivery result back, that is the difference between an integration you maintain and a tool you expose once. See RCS and AI agents.
What to compare when evaluating an API
Beyond endpoints, the questions worth asking are: is fallback automatic and reported per channel; are webhooks signed, idempotent, and replayable; are credentials environment-scoped; does the provider handle brand and carrier registration or hand you the paperwork; and is the API priced the same as the dashboard. On SimplyRCS the API and the app carry the same capabilities at the same published price.