How to Integrate RCS Business Messaging API in PHP (With Webhooks & Fallback)
A practical PHP walkthrough: authenticate, send rich cards and carousels with reply buttons, receive delivery-status webhooks and fall back to SMS when RCS can't be delivered.
Key points
- Send RCS messages as JSON over HTTPS with an Authorization: Bearer header, from plain PHP and cURL.
- Rich cards and carousels are nested arrays: title, description, media and up to four suggestions per card.
- Delivery status arrives on your webhook in real time; answer 200 OK quickly and store it by message ID.
- If RCS is not delivered, your system or gateway workflow routes the message to DLT-approved SMS.
- The code here is illustrative. Onboarded customers get the exact endpoints and fields with their API access.
Why developers are moving from SMS APIs to RCS
A typical SMS API takes a phone number and a string of text. That covers OTPs and alerts, but every link is a bare URL and every reply is free text your code has to parse. RCS Business Messaging keeps the same send-and-track model and adds structure:
- Structured content: a JSON payload describes the card: image, title, description and buttons. The phone renders it natively in the default messaging app.
- Machine-readable replies: each suggested reply carries a postback value, so your code gets
BUY_SHOESinstead of “yes pls”. - Per-message events: delivery status arrives on your webhook as it happens, keyed by the message ID returned when you send.
- Verified sender: messages come from your verified brand agent, with name and logo, rather than a bare sender ID.
The examples below use plain PHP 8 with the cURL extension, so they drop into Laravel, WordPress, CodeIgniter or a cron script without extra packages.
Before you start
API access is issued when your account is onboarded: the API host, your credentials, your verified agent and your approved message templates. The code in this article is illustrative. The host (API_HOST), the /v1/messages path, field names such as template_id and the status names show the shape of an integration; use the exact values from the API reference you receive with your access.
Endpoint and authentication
Requests are JSON over HTTPS. Each one carries your access token in the Authorization header:
Authorization: Bearer <YOUR_ACCESS_TOKEN>With cURL on the command line, keep the token in an environment variable rather than typing it into scripts or shell history:
curl -X POST "https://API_HOST/v1/messages" \
-H "Authorization: Bearer $RCS_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d @rich-card.jsonIn PHP, a small helper does the same. It reads the token from the environment, sends the payload, treats any non-2xx response as an error and returns the decoded JSON, which includes the message ID you will see again in webhooks.
<?php
// rcs-client.php: a small helper for sending RCS messages over HTTPS.
declare(strict_types=1);
const RCS_API_BASE = 'https://API_HOST'; // from your onboarding pack
function sendRcs(array $payload): array
{
$token = getenv('RCS_ACCESS_TOKEN'); // keep credentials out of source code
if ($token === false || $token === '') {
throw new RuntimeException('Set the RCS_ACCESS_TOKEN environment variable.');
}
$ch = curl_init(RCS_API_BASE . '/v1/messages'); // example path
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new RuntimeException("Request failed: $error");
}
if ($status < 200 || $status >= 300) {
throw new RuntimeException("API returned HTTP $status");
}
return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}Sending rich cards and carousels
A single rich card
A rich card combines media, a title, a short description and up to four suggestions: reply chips (which send a postback to your webhook) and actions such as opening a URL. This order update offers tracking and a slot change:
<?php
require __DIR__ . '/rcs-client.php';
$richCard = [
'to' => '+91XXXXXXXXXX',
'template_id' => 'YOUR_TEMPLATE_ID', // an approved template
'type' => 'rich_card',
'card' => [
'title' => 'Your order #10234 is packed',
'description' => 'Arriving Friday. Track it or pick another delivery slot.',
'media' => ['url' => 'https://example.com/images/order-10234.jpg', 'height' => 'MEDIUM'],
'suggestions' => [
['type' => 'url', 'text' => 'Track order', 'url' => 'https://example.com/track/10234'],
['type' => 'reply', 'text' => 'Change slot', 'postback' => 'CHANGE_SLOT_10234'],
],
],
];
$result = sendRcs($richCard);
echo 'Accepted, message ID: ' . ($result['message_id'] ?? 'see response') . PHP_EOL;A carousel of cards
A carousel is a row of two to ten cards that customers swipe through. Build the cards from your own data; here, three products, each with “Buy now” and “More details” replies whose postbacks identify the product:
<?php
require __DIR__ . '/rcs-client.php';
$products = [
['Running shoes', 'Sizes 6 to 11, free returns', 'https://example.com/img/shoes.jpg', 'SHOES'],
['Smart watch', 'Two-year warranty', 'https://example.com/img/watch.jpg', 'WATCH'],
['Backpack', '30 litres, water resistant', 'https://example.com/img/bag.jpg', 'BAG'],
];
$carousel = [
'to' => '+91XXXXXXXXXX',
'template_id' => 'YOUR_CAROUSEL_TEMPLATE_ID',
'type' => 'carousel',
'card_width' => 'MEDIUM',
'cards' => array_map(fn (array $p): array => [
'title' => $p[0],
'description' => $p[1],
'media' => ['url' => $p[2], 'height' => 'MEDIUM'],
'suggestions' => [
['type' => 'reply', 'text' => 'Buy now', 'postback' => 'BUY_' . $p[3]],
['type' => 'reply', 'text' => 'More details', 'postback' => 'INFO_' . $p[3]],
],
], $products),
];
$result = sendRcs($carousel);
echo 'Accepted, message ID: ' . ($result['message_id'] ?? 'see response') . PHP_EOL;Templates are reviewed before you can send them, so register the card layout first and keep the content you send consistent with the approved template.
Receiving real-time delivery webhooks
Give your provider an HTTPS URL. Each status change is sent there as an HTTP POST with a JSON body along these lines:
{
"message_id": "MSG_ID_FROM_SEND_RESPONSE",
"status": "DELIVERED",
"timestamp": "2026-10-22T09:30:12Z"
}The receiver below accepts only POST requests, checks that the callback is genuine, records DELIVERED, READ and FAILED by message ID, queues an SMS fallback on failure and always answers 200 OK for a valid request, so the sender doesn't keep retrying.
<?php
// rcs-webhook.php: receives delivery-status callbacks and answers 200 OK.
declare(strict_types=1);
const KNOWN_STATUSES = ['DELIVERED', 'READ', 'FAILED']; // map to your provider's values
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
header('Allow: POST');
exit;
}
// Only trust callbacks from your provider. This example compares a shared secret sent in a
// header; use the check described in your onboarding pack.
$secret = getenv('RCS_WEBHOOK_SECRET') ?: '';
$given = $_SERVER['HTTP_X_WEBHOOK_TOKEN'] ?? '';
if ($secret === '' || !hash_equals($secret, $given)) {
http_response_code(401);
exit;
}
try {
$event = json_decode(file_get_contents('php://input') ?: '', true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
http_response_code(400);
exit;
}
$messageId = is_array($event) ? (string) ($event['message_id'] ?? '') : '';
$status = is_array($event) ? strtoupper((string) ($event['status'] ?? '')) : '';
if ($messageId !== '' && in_array($status, KNOWN_STATUSES, true)) {
saveStatus($messageId, $status);
if ($status === 'FAILED') {
queueSmsFallback($messageId);
}
}
// Acknowledge quickly, even for statuses you don't use, so the provider stops retrying.
http_response_code(200);
header('Content-Type: text/plain');
echo 'OK';
function saveStatus(string $messageId, string $status): void
{
// Upsert by message ID so a repeated callback is harmless. Log IDs and statuses only,
// never phone numbers or message text.
error_log("RCS status: $messageId $status");
}
function queueSmsFallback(string $messageId): void
{
// Add a job to your queue. The worker sends the DLT-approved SMS (registered header and
// template) only if the RCS message still hasn't been delivered.
}A few rules keep webhook handling reliable:
- Respond within a few seconds. Do slow work, such as sending SMS or updating a CRM, in a queue.
- Expect the same event more than once and store it idempotently by message ID.
- Events can arrive out of order; don't let a late DELIVERED overwrite READ.
- Never log phone numbers or message text. The message ID is enough to trace a problem.
DLT compliance and SMS fallback
RCS delivery status is tracked in real time via webhooks. If an RCS payload is undelivered, for example because the phone is offline or the handset doesn't support RCS, your system or gateway workflow can automatically route the fallback message via standard DLT-approved bulk SMS.
In practice, the fallback rule is a small decision: delivered or read means done, failed means send SMS, and no update within your time limit means revoke the RCS message and then send SMS, so the customer doesn't get both.
<?php
// Decide whether a message should fall back to SMS.
function needsSmsFallback(?string $status, int $sentAt, int $now, int $waitSeconds = 600): bool
{
if ($status === 'DELIVERED' || $status === 'READ') {
return false; // RCS reached the phone
}
if ($status === 'FAILED') {
return true; // for example, the handset doesn't support RCS
}
return $now - $sentAt >= $waitSeconds; // no delivery in time: revoke the RCS message, then send SMS
}DLT rules apply on both paths in India:
- In compliance with TRAI guidelines, enterprise brands sending commercial messages over RCS must maintain an active DLT registration. Header names and message templates are validated against DLT records before campaign submission.
- The fallback SMS must use your registered DLT sender header and an approved DLT template, so register an SMS version of each message you plan to fall back.
- Send only to customers who have agreed to hear from you, in the right category.
Get API access
Onboarded customers receive the API host, credentials and full reference for sending and webhooks. Request access, or talk to us about your integration.