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.

PathWhat you use SimplyRCS forWhat stays in your systems
Gateway / ConnectivityMessage sending, inbound events, delivery receipts, and supported sender operationsYour CRM, conversation logic, and UI
ApplicationContacts, segments, blasts, bots, acquisition campaigns, and analytics on top of messagingWhatever 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

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.

TaskCustomer API pathConsole handoff / limitation
Create the workspace and first userNo public API-key bootstrap contractSign up, then complete Company Profile
Create the first API keyAn existing key is needed to authenticate key-management API callsDeveloper Access for the initial key
List, create, rotate, or revoke later API keys/v1/api-keys and its documented operationsSame 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-endpointsUse 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 operationsMessage reports / Reports are optional views
Sender inventory and supported configuration/v1/sender-ids, /v1/programs, and the operations enabled for your accountSenders & Numbers for channel-specific setup and readiness
Business identity, KYC, and carrier/agent filingOnly explicitly supported customer operations; availability is workflow-specificBrands & 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 settingsNot part of the current filtered customer API contractBilling; use the secure browser payment flow, not raw card data in messaging API requests
Team, workspace settings, and account administrationDo not use private browser handlers as a public APIAccount 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.

  1. Create an API key (above).
  2. Select a configured sender. List /v1/sender-ids and 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.
  3. 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}/launch operation is not a public go-live contract: governed registrations are fenced, and a local LAUNCH_PENDING phase 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.
  4. Attach the sender to a messaging program with POST /v1/programs/{id}/senders so readiness checks pass. GET /v1/programs shows readiness.
  5. 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\"}" }
        ]
      }
    }
  }'
  • to takes one E.164 number per call. Send one request per recipient, each with its own Idempotency-Key.
  • media.type is image, video, audio, or file; url must be publicly fetchable by the carrier network. thumbnail is optional.
  • actions[].type is reply for a quick-reply chip. label is what the handset shows; value is your own routing token and comes back untouched as buttonPayload on the button.clicked webhook event (which also emits message.received). Other action types are url, call, location, and calendar; those take their target in value.
  • 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/send addressed to that sender returns 409 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/dnc and set per-contact TCPA flags with PATCH /v1/contacts/{id}/tcpa. Sends to blocked contacts fail closed.
  • Reconciliation exports. GET /v1/reports/messages returns inbound messages and delivery receipts for a date range.
  • Voice on your number. PUT /v1/sender-ids/{id}/voice-forwarding forwards 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:

HeaderValue
Content-Typeapplication/json
User-AgentRCS-Studio-Webhook/1.0
X-SignatureLowercase hex HMAC-SHA256 of the raw request body, keyed with the endpoint secret
X-Webhook-Delivery-IdUnique 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 2xx within 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-Signature value is identical on every attempt of one event. Use it, or a hash of the raw body, as your deduplication key. X-Webhook-Delivery-Id changes per attempt and is for support lookups, not deduplication.
  • Do not deduplicate on event plus an entity id. message.status fires once per transition for the same messageId (SENT, then DELIVERED, then SEEN), and each of those is a distinct event you want. Two deliveries with the same messageId but different status are not duplicates.
  • Delivery order is not guaranteed. A message.status for DELIVERED can arrive before the one for SENT. Treat status as 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 with PATCH /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:

EventWhenKey fields in data
message.receivedInbound 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.statusStatus change on a message you sent. QUEUED → SENT → DELIVERED → SEEN; FAILED is terminal.messageId, conversationId, previousStatus, status, channelType
message.revokedAn RCS message was recalled.messageId, conversationId, contactId, revokedAt, revokedBy
button.clickedContact tapped a suggested reply or action. Also emits message.received.messageId, conversationId, contactId, buttonPayload, channelType
handoffA bot or the built-in agent handed the conversation to a human.conversationId, contactId, botId, channelId, reason, source
blast.completeThe sending phase completed. Counts are a snapshot at emission; later delivery/read receipts can change totals.blastId, name, total, sent, delivered, failed
person.acquiredContact enrolled via keyword, web form, QR code, or API campaign.contactId, phone, campaignId, listId, status
person.subscribedEnrollment confirmed.contactId, phone, campaignId, listId, consentType, subscribedAt
event.sent / event.failed / event.skippedOutcome 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 with POST /v1/contacts/bulk/tag, and target with POST /v1/segments/resolve.
  • Blasts. Create, estimate, test, schedule, and launch under /v1/blasts. blast.complete tells you when it is done.
  • Bots. Build and publish conversational flows under /v1/bots; handoff tells you when a person is needed.
  • Acquisition campaigns. Keyword, QR, web-form, and API enrollment under /acquisition-campaigns, with person.acquired and person.subscribed as the feedback loop.
  • Event-triggered messaging. POST /v1/events maps 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. MARKETING sends need an opt-in on record; TRANSACTIONAL and SERVICE are 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.json in this folder is an offline copy of the spec generated from the development branch. Regenerate it with npm run docs:openapi. The live spec at /api/openapi/customer is always current for the environment you are talking to.