SimplyRCS API Integration Guide
Use SimplyRCS from your own backend for RCS and SMS messaging, while retaining your own CRM, conversation logic, and customer interface. The application features are optional; having an endpoint in the reference does not mean every sender or carrier workflow is enabled for your account.
| Path | What you use SimplyRCS for | What stays in your systems |
|---|---|---|
| Gateway / Connectivity | Message sending, inbound events, delivery receipts, and supported sender operations | Your CRM, conversation logic, and UI |
| Application | Contacts, segments, blasts, bots, acquisition campaigns, and analytics on top of messaging | Whatever you choose |
Both paths currently use the same customer API. Start with the gateway path; application features are additive, not prerequisites for operating your own messaging interface. This guide describes the hosted integration, not a separately extracted or self-hosted Connectivity product.
Start here
- Interactive reference: Customer API reference
- Machine-readable OpenAPI 3.1 spec: Customer OpenAPI
- Application workflows: Application guide
- Development base URL:
https://simplyrcs.dev.codeflexlabs.net
The reference page shows the API base URL for the environment you opened. Use its matching API key and configured senders; do not send production traffic to the development environment. Production access and each sender's approval are separate from availability of these public docs. The examples below use an explicit environment variable:
export SIMPLYRCS_API_BASE_URL="https://simplyrcs.dev.codeflexlabs.net"
The spec includes a Webhooks section that documents every event payload SimplyRCS sends to you. Import the spec into Postman, Bruno, Insomnia, or a client generator to get request and response types.
Use only operations present in the customer OpenAPI contract. Most are under /v1/*; do not guess paths from the browser's network requests. Private /api/* application handlers, platform-admin endpoints, and provider callback routes are not customer integration APIs.
API or console: choose the right surface
The console is the setup and account-management companion to your API integration. These links require your normal browser login; never put an API key in a link or expect a server API key to establish a browser session.
| Task | Customer API path | Console handoff / limitation |
|---|---|---|
| Create the workspace and first user | No public API-key bootstrap contract | Sign up, then complete Company Profile |
| Create the first API key | An existing key is needed to authenticate key-management API calls | Developer Access for the initial key |
| List, create, rotate, or revoke later API keys | /v1/api-keys and its documented operations | Same Developer Access page; keys remain limited by access scope and resource permissions |
| Send RCS or SMS; receive messages and receipts | /v1/messages/send, /v1/conversations/{id}/messages, /v1/webhook-endpoints | Use an approved, configured sender; the console Inbox is optional for your integration |
| Message history, receipts, and reporting | /v1/conversations/{id}/messages, /v1/reports/messages, and documented /v1/analytics operations | Message reports / Reports are optional views |
| Sender inventory and supported configuration | /v1/sender-ids, /v1/programs, and the operations enabled for your account | Senders & Numbers for channel-specific setup and readiness |
| Business identity, KYC, and carrier/agent filing | Only explicitly supported customer operations; availability is workflow-specific | Brands & KYC, Senders & Numbers, and Submissions. A local review or phase change is not carrier approval |
| Add/change a credit card, fund the account, view invoices and billing settings | Not part of the current filtered customer API contract | Billing; use the secure browser payment flow, not raw card data in messaging API requests |
| Team, workspace settings, and account administration | Do not use private browser handlers as a public API | Account settings and its role-appropriate controls |
Gateway reporting is therefore available by API; full billing/account administration is not promised as API-only. API access never bypasses consent, pricing, funding, sender approval, or account permissions.
Authentication
Create a key in the app under Developer > API Keys, or with POST /v1/api-keys once you have a first key. Send it either way:
curl "$SIMPLYRCS_API_BASE_URL/v1/sender-ids" \
-H "Authorization: Bearer $SIMPLYRCS_API_KEY"
curl "$SIMPLYRCS_API_BASE_URL/v1/sender-ids" \
-H "X-API-Key: $SIMPLYRCS_API_KEY"
Keys are secrets. Keep them server-side, never in browser or mobile code. Rotate with POST /v1/api-keys/{id}/rotate.
Gateway path
Account setup
Do these once per environment. Carrier approval is asynchronous and workflow-specific; the API does not make a sender immediately live.
- Create an API key (above).
- Select a configured sender. List
/v1/sender-idsand its protocols. RCS and SMS require their respective approved routes. For a new sender, use Senders & Numbers and the supported channel-specific workflow; inventory, purchase/import, and provider registration are different operations, not interchangeable shortcuts. - Complete the applicable review. Follow business verification in Brands & KYC and sender/agent work in Senders & Numbers and Submissions. Do not apply an SMS registration recipe to an RCS agent. Governed RCS creation/testing and an already adopted live sender have separate eligibility rules. The legacy
/v1/sender-ids/{id}/launchoperation is not a public go-live contract: governed registrations are fenced, and a localLAUNCH_PENDINGphase does not submit or approve a carrier launch. Standard self-service public RCS launch remains unavailable; use only the sender/testing path explicitly enabled for your account. The console does not bypass this limitation. - Attach the sender to a messaging program with
POST /v1/programs/{id}/sendersso readiness checks pass.GET /v1/programsshows readiness. - Register a webhook endpoint (next section). This is how inbound messages and delivery receipts reach you.
Runtime calls
Address sends by phone number. Put the recipient's E.164 number in to. SimplyRCS looks up the contact by phone within your account and creates a minimal one if it does not exist, then returns the resolved contactId in the response. Your CRM stays the source of truth; SimplyRCS only needs the phone. If you already hold a contact id, pass contactId instead. Never both.
Send. Pass an Idempotency-Key. Within its 24-hour retention window, the same key and body replay a stored successful response; an attempt still in progress returns 409. A 502 PROVIDER_ACCEPTANCE_UNCERTAIN means the provider may have accepted the message but acceptance could not be confirmed or recorded locally. Its reservation stays in progress: do not bypass it with a new key or resend after expiry without reconciling delivery/provider evidence. This is not an unlimited exactly-once guarantee. Set messageType deliberately: the default is MARKETING, which applies the strictest consent gates. Use TRANSACTIONAL or SERVICE for order updates, alerts, and replies.
curl "$SIMPLYRCS_API_BASE_URL/v1/messages/send" \
-H "Authorization: Bearer $SIMPLYRCS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <unique-request-id>" \
-d '{
"to": "+15551234567",
"senderIdId": "SENDER_ID",
"protocolType": "RCS_RICH",
"fallbackToSms": true,
"messageType": "TRANSACTIONAL",
"content": { "type": "text", "text": "Your order 8812 has shipped." }
}'
content.type can be text, card, carousel, media, or location, with optional actions for suggested replies. Use the reference for each content shape. A capability check is diagnostic evidence, not sender approval or a guarantee of delivery.
One-off rich message: text, media, and quick replies
You do not need a bot to send a single message. If you are moving from an API that takes text, a media URL, and quick replies in one payload, send a card: it carries a title, description, one media item, and up to 11 suggestions together. A media message carries only the file, with no text or suggestions.
curl "$SIMPLYRCS_API_BASE_URL/v1/messages/send" \
-H "Authorization: Bearer $SIMPLYRCS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <unique-request-id>" \
-d '{
"to": "+15551234567",
"senderIdId": "SENDER_ID",
"protocolType": "RCS_RICH",
"messageType": "TRANSACTIONAL",
"content": {
"type": "card",
"card": {
"title": "Thanks for your interest",
"description": "Your appointment is coming up.",
"media": { "type": "video", "url": "https://cdn.example.com/welcome.mp4" },
"actions": [
{ "type": "reply", "label": "I will be there", "value": "{\"event\":\"appointment_confirm\"}" },
{ "type": "reply", "label": "Reschedule", "value": "{\"event\":\"appointment_reschedule\"}" }
]
}
}
}'
totakes one E.164 number per call. Send one request per recipient, each with its ownIdempotency-Key.media.typeisimage,video,audio, orfile;urlmust be publicly fetchable by the carrier network.thumbnailis optional.actions[].typeisreplyfor a quick-reply chip.labelis what the handset shows;valueis your own routing token and comes back untouched asbuttonPayloadon thebutton.clickedwebhook event (which also emitsmessage.received). Other action types areurl,call,location, andcalendar; those take their target invalue.- For text with chips and no media, use
"content": { "type": "text", "text": "...", "actions": [ ... ] }. - A 200 response means the carrier network accepted the message, not that it was delivered. Delivery arrives on
message.status.
While your RCS sender is in testing
A new RCS sender starts in the carrier network's testing stage and stays there until launch is requested and the carriers approve it. During that stage:
POST /v1/messages/sendaddressed to that sender returns409 RCS_GOVERNED_WORKFLOW_REQUIRED("This RCS Sender is ready for testing, not live"). The public API cannot send from a sender that is not launched, not even to a test device.- Handset testing happens in the console. Register each tester's phone under Senders & Numbers, open the sender, Testing tab, and have the tester accept on the phone. Then send from Inbox (New message, choosing the sender marked "Ready for testing (test devices only)") or from the Bot testing tools. Only accepted test devices can receive.
- Build and test your integration against the API shapes above; the same call succeeds unchanged once the sender is launched and adopted as live.
SMS as the primary channel
Choose an approved SMS sender and request protocolType: "SMS" explicitly. An RCS sender identifier alone does not establish an eligible SMS route.
curl "$SIMPLYRCS_API_BASE_URL/v1/messages/send" \
-H "Authorization: Bearer $SIMPLYRCS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <another-unique-request-id>" \
-d '{
"to": "+15551234567",
"senderIdId": "SMS_SENDER_ID",
"protocolType": "SMS",
"messageType": "TRANSACTIONAL",
"content": { "type": "text", "text": "Your order 8812 has shipped." }
}'
RCS fallback: what the option does
fallbackToSms: true enables the existing fallback planner when the RCS provider send fails synchronously with an eligible, definite rejection. A rate-limited RCS send does not take that fallback path. Infobip RCS timeouts, lost responses, server errors, or missing tracking IDs are uncertain outcomes, not permission to send another message; no fallback is dispatched for them. Eligible, configured fallback routes and consent/pricing checks are still required; rich content can attempt MMS before SMS. Read fallbackUsed and fallbackChannelId in the response when present.
This is not a timeout-based or failed-delivery-receipt guarantee: an accepted RCS request that later receives a failed or missing receipt does not automatically enter this direct-send fallback path. Timed Blast fallback is a separate application workflow. Process message.status events and do not submit a second SMS just because a receipt is delayed; the original RCS message may still arrive. Never infer delivery from an HTTP success alone.
The response includes messageId, conversationId, and the resolved contactId.
Reply in a thread. Every inbound message arrives with a conversationId. Answer on it with POST /v1/conversations/{id}/messages; you do not need to repeat the routing fields.
Recall an RCS message with POST /v1/messages/{id}/revoke.
Receive. Inbound messages, delivery receipts, and button taps arrive on your webhook endpoint. There is no polling API for inbound traffic; GET /v1/conversations/{id}/messages is for history.
Optional gateway extras
- Opt-out enforcement inside SimplyRCS. Add numbers to the do-not-contact list with
POST /v1/compliance/dncand set per-contact TCPA flags withPATCH /v1/contacts/{id}/tcpa. Sends to blocked contacts fail closed. - Reconciliation exports.
GET /v1/reports/messagesreturns inbound messages and delivery receipts for a date range. - Voice on your number.
PUT /v1/sender-ids/{id}/voice-forwardingforwards calls to the number to a destination you choose.
Webhooks
Register an endpoint
curl "$SIMPLYRCS_API_BASE_URL/v1/webhook-endpoints" \
-H "Authorization: Bearer $SIMPLYRCS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/simplyrcs/webhooks",
"events": ["message.received", "message.status", "button.clicked"]
}'
The 201 response includes secret. It is returned once. Store it; you need it to verify every delivery. To rotate, delete the endpoint and create a new one.
Destinations must be public HTTPS URLs without embedded credentials. Private, loopback, and link-local addresses are rejected. Redirects are followed only when they stay on the same origin.
Delivery format
Every delivery is an HTTPS POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | RCS-Studio-Webhook/1.0 |
X-Signature | Lowercase hex HMAC-SHA256 of the raw request body, keyed with the endpoint secret |
X-Webhook-Delivery-Id | Unique per delivery attempt; matches id in the deliveries log |
The body is always the same envelope:
{
"event": "message.received",
"data": { "...event-specific fields..." },
"timestamp": "2026-09-08T16:40:12.345Z",
"orgId": "org_123"
}
brandId and campaignId appear in the envelope only when your account uses multi-brand scoping.
Verify the signature
Compute the HMAC over the raw bytes you received, before JSON parsing, and compare with a constant-time function.
Node:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifySimplyRcsSignature(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(signatureHeader ?? '', 'utf8');
return a.length === b.length && timingSafeEqual(a, b);
}
Python:
import hmac, hashlib
def verify_simplyrcs_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
Make sure your framework gives you the unparsed body. Re-serializing parsed JSON changes whitespace and key order and the signature will not match.
Acknowledge, retries, and failure handling
- Respond with any
2xxwithin 10 seconds. Do your processing after you respond, or keep it under the limit. - Anything else (non-2xx, timeout, connection error) is retried. Each event gets 4 attempts total with exponential backoff starting at 5 seconds, so roughly 5 s, 10 s, and 20 s between attempts.
- Retries resend the exact same body bytes, so the
X-Signaturevalue is identical on every attempt of one event. Use it, or a hash of the raw body, as your deduplication key.X-Webhook-Delivery-Idchanges per attempt and is for support lookups, not deduplication. - Do not deduplicate on
eventplus an entity id.message.statusfires once per transition for the samemessageId(SENT, thenDELIVERED, thenSEEN), and each of those is a distinct event you want. Two deliveries with the samemessageIdbut differentstatusare not duplicates. - Delivery order is not guaranteed. A
message.statusforDELIVEREDcan arrive before the one forSENT. Treatstatusas the current state and ignore transitions that would move backwards. - After 10 consecutive failed deliveries the endpoint is disabled (
isActive: false) and nothing further is sent until you re-enable it withPATCH /v1/webhook-endpoints/{id}and{ "isActive": true }. Successful deliveries reset the counter. - Payloads over 256 KB are dropped, not truncated. In practice only very large inbound media descriptions approach this.
Test and debug
POST /v1/webhook-endpoints/{id}/test sends one signed delivery synchronously with data: { "test": true } and returns your status code and response time. Use it to check signature verification before real traffic.
GET /v1/webhook-endpoints/{id}/deliveries lists every attempt with the status code you returned and a truncated copy of your response body.
Events
The spec's Webhooks section is the authoritative schema for each data object. Summary:
| Event | When | Key fields in data |
|---|---|---|
message.received | Inbound message on one of your senders. Unknown numbers are auto-created as contacts first. | messageId, conversationId, contactId, channelId, channelType, contentJson (text, media, location, or suggestion) |
message.status | Status change on a message you sent. QUEUED → SENT → DELIVERED → SEEN; FAILED is terminal. | messageId, conversationId, previousStatus, status, channelType |
message.revoked | An RCS message was recalled. | messageId, conversationId, contactId, revokedAt, revokedBy |
button.clicked | Contact tapped a suggested reply or action. Also emits message.received. | messageId, conversationId, contactId, buttonPayload, channelType |
handoff | A bot or the built-in agent handed the conversation to a human. | conversationId, contactId, botId, channelId, reason, source |
blast.complete | The sending phase completed. Counts are a snapshot at emission; later delivery/read receipts can change totals. | blastId, name, total, sent, delivered, failed |
person.acquired | Contact enrolled via keyword, web form, QR code, or API campaign. | contactId, phone, campaignId, listId, status |
person.subscribed | Enrollment confirmed. | contactId, phone, campaignId, listId, consentType, subscribedAt |
event.sent / event.failed / event.skipped | Outcome of a POST /v1/events submission. | eventLogId, eventType, contactId, messageId or reason |
For a gateway integration, subscribe to message.received, message.status, and button.clicked.
Delivery receipts arrive as message.status. The names message.delivered, message.read, and message.failed are accepted when you create an endpoint, but they are reserved and are not emitted today. The same applies to blast.completed, blast.failed, contact.created, contact.updated, contact.opted_out, session.started, session.expired, payment.succeeded, and payment.failed. Subscribing to them does no harm; they just will not fire.
The scoped-suppression contract defines contact.opted_out for a newly recorded
STOP or STOP ALL, but delivery remains inactive until authenticated inbound
keyword handling is enabled. When enabled, data.eventId is stable across
retries and is the receiver's deduplication key. The event includes the stopped
phone, affected scope and class, receiving sender, channel, keyword and commit
time. The org/brand/campaign envelope fields describe the originating sender
context; they do not broaden the affected scope. Endpoint audience is fixed when
the event is recorded and rechecked before delivery. See the
scoped-suppression contract for the
exact payload and recipient rules.
Application path
Everything in the gateway path, plus:
- Contacts and audiences. Bulk import with
POST /v1/contacts/import, tag withPOST /v1/contacts/bulk/tag, and target withPOST /v1/segments/resolve. - Blasts. Create, estimate, test, schedule, and launch under
/v1/blasts.blast.completetells you when it is done. - Bots. Build and publish conversational flows under
/v1/bots;handofftells you when a person is needed. - Acquisition campaigns. Keyword, QR, web-form, and API enrollment under
/acquisition-campaigns, withperson.acquiredandperson.subscribedas the feedback loop. - Event-triggered messaging.
POST /v1/eventsmaps your application events to templated sends. - Analytics. Overview, per-sender, per-bot, and per-blast reporting under
/v1/analytics, plus exports.
Behavior to know before launch
- Consent and quiet-hour rules run before every send and fail closed.
MARKETINGsends need an opt-in on record;TRANSACTIONALandSERVICEare less restricted but still honor opt-outs. - Test billing behavior applies only to explicitly designated test execution paths. Registering a handset as a test device does not exempt ordinary live traffic to that number from billing.
- The checked-in
openapi.customer.dev.jsonin this folder is an offline copy of the spec generated from the development branch. Regenerate it withnpm run docs:openapi. The live spec at/api/openapi/customeris always current for the environment you are talking to.