Skip to content

Rich messages

Rich messages are the reason to use RCS at all: an image, a card, a swipeable row of cards, and buttons that dial a number, open a link or drop an event in the calendar.

They all go through the same endpoint as plain text — POST /ng/v1/rcs/messages — and adding any of them switches the message to the rich path automatically.

suggestions work alongside plain content. No card to design, no layout to get right — just make the notification actionable.

{
"senderId": "FirstDigits",
"destination": "2348020000000",
"content": "Your order #AB123 is out for delivery.",
"suggestions": [
{
"reply": {
"displayText": "Track it",
"postback": { "data": "track:AB123" }
}
},
{
"action": {
"displayText": "Call support",
"postback": { "data": "call_support" },
"dialerAction": {
"dialPhoneNumber": { "phoneNumber": "+2348000000000" }
}
}
}
]
}

Each chip is either a reply or an action, never both.

displayText and postback.data are independent — on purpose

The user sees displayText. Your agent receives postback.data.

That means the label can change — translation, a rewrite, A/B copy — without breaking the code that handles the tap. Put a stable identifier in data, not the label:

{ "displayText": "Track it", "postback": { "data": "track:AB123" } }

An action chip does something on the handset as well as posting back. Set exactly one:

FieldWhat it does
urlAction.openUrl.urlOpens a link.
dialerAction.dialPhoneNumber.phoneNumberStarts a call. International format, with the +.
calendarAction.createCalendarEventOffers to create an event — startTime, endTime, title, optional description. Times are RFC 3339.
mapAction.showLocationPins a place — location.latitude, location.longitude, optional label and fallbackUrl.
mapAction.requestLocationPushAsks the user to share their location. Pass {}.
{
"action": {
"displayText": "Add to calendar",
"postback": { "data": "cal:appt-9931" },
"calendarAction": {
"createCalendarEvent": {
"startTime": "2026-09-14T09:00:00Z",
"endTime": "2026-09-14T09:30:00Z",
"title": "Dental check-up",
"description": "Bring your card."
}
}
}
}

richCard is media plus a title, a description, and its own chips.

Terminal window
curl -X POST https://api.9bits.net/ng/v1/rcs/messages \
-H "Authorization: Bearer $NINEBITS_RCS_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderId": "FirstDigits",
"destination": "2348020000000",
"content": "Your order has shipped",
"richCard": {
"layout": { "cardOrientation": "VERTICAL" },
"content": {
"title": "Order AB123 shipped",
"description": "Arriving Thursday between 9am and 12pm.",
"media": {
"mediaUrl": "https://cdn.example.com/parcel.jpg",
"mediaContentType": "image/jpeg",
"mediaFileSize": 148213,
"height": "MEDIUM_HEIGHT",
"contentDescription": "A parcel on a doorstep"
},
"suggestions": [
{
"action": {
"displayText": "Track parcel",
"postback": { "data": "track:AB123" },
"urlAction": { "openUrl": { "url": "https://example.com/track/AB123" } }
}
}
]
}
}
}'
FieldValuesNotes
layout.cardOrientationVERTICAL, HORIZONTALVertical puts the media above the text.
layout.imageAlignmentLEFT, RIGHTHORIZONTAL cards only.

Two or more cards the user swipes through. Same content shape as a single card, in a list.

{
"senderId": "FirstDigits",
"destination": "2348020000000",
"carousel": {
"layout": { "cardOrientation": "VERTICAL", "cardWidth": "MEDIUM_WIDTH" },
"content": [
{
"title": "Lagos → Abuja",
"description": "From ₦48,000 return",
"media": {
"mediaUrl": "https://cdn.example.com/abuja.jpg",
"mediaContentType": "image/jpeg",
"mediaFileSize": 201553,
"height": "MEDIUM_HEIGHT"
}
},
{
"title": "Lagos → Accra",
"description": "From ₦92,000 return",
"media": {
"mediaUrl": "https://cdn.example.com/accra.jpg",
"mediaContentType": "image/jpeg",
"mediaFileSize": 187004,
"height": "MEDIUM_HEIGHT"
}
}
]
}
}
FieldValuesNotes
layout.cardOrientationVERTICALVertical in practice.
layout.cardWidthSMALL_WIDTH, MEDIUM_WIDTHThe knob that actually matters.
{
"mediaUrl": "https://cdn.example.com/parcel.jpg",
"mediaContentType": "image/jpeg",
"mediaFileSize": 148213,
"height": "MEDIUM_HEIGHT",
"contentDescription": "A parcel on a doorstep"
}

Four things worth knowing:

  • mediaUrl must be publicly reachable. The platform fetches it — a URL behind your VPN, your auth, or localhost produces a card with a hole in it.
  • mediaFileSize is the size in bytes and it is required. It is not derived for you: fetching the URL just to measure it would put a network round trip into every send, for a number you already have.
  • height is SHORT_HEIGHT, MEDIUM_HEIGHT or TALL_HEIGHT. TALL_HEIGHT is rejected on a carousel.
  • contentDescription is alt text, read out by screen readers. Set it.

Optional thumbnailUrl, thumbnailContentType and thumbnailFileSize follow the same rules.

A handset without RCS cannot render any of this. Rich sends do not silently downgrade to SMS — watch for Undelivered in the delivery report and send the SMS version yourself when it arrives.

Writing the plain-text equivalent first, then enriching it, tends to be the easier order: you end up with a fallback you have already tested.