How to Send RCS Messages
Send rich, branded conversations to your customers through Google's Rich Communication Services (RCS). The LigueLead API supports four template variants — plain text, single media, interactive rich cards, and horizontal carousels — created with a single request and reusable across every send.
Send RCS messages with the appropriate template type and understand the credits charged for each message.
📋 Prerequisites
- Get a valid
api-tokenandapp-idfrom your LigueLead account, or anapp-idcreated through the API. - Ask your account manager to enable RCS for the
app-idyou will use. Creating an app through the API does not enable RCS for it; your account manager must still enable RCS for thatapp-id. Devices that do not support RCS receive thefallback_message, when provided, through the SMS gateway. - Format target phone numbers as Brazilian national 11-digit numbers, international numbers with
+55, or DDI numbers.
💳 RCS billing
RCS messages are charged per recipient according to the message type and, for text, its size in bytes.
| Message type | Rule | Credits charged | Rate |
|---|---|---|---|
| Text — freeform or text template | Up to 160 bytes | 1 RCS Basic credit | R$ 0.10 |
| Text — freeform or text template | More than 160 bytes | 2 RCS Basic credits | R$ 0.20 |
| Media template | Any text size | 1 RCS Single credit | R$ 0.15 |
| Interactive card template | Any text size | 1 RCS Single credit | R$ 0.15 |
| Carousel template | Any text size | 1 RCS Single credit | R$ 0.15 |
The 160-byte threshold applies to freeform messages and text templates. Bytes are not the same as characters: accented characters, emojis, and other Unicode characters can use more than one byte. As a result, messages with the same character count can fall into different billing tiers.
For example, a text message with 160 bytes or fewer consumes one RCS Basic credit. A text message above 160 bytes consumes two RCS Basic credits. Media, card, and carousel templates always consume one RCS Single credit.
🧭 Choose the right template type
| Template | Endpoint | When to use |
|---|---|---|
| Text | POST /v1/rcs/templates/text | Plain branded notifications with {{N}} placeholders. No media, no buttons. |
| Media | POST /v1/rcs/templates/media | Notification with a single image or short video plus a body caption. |
| Card | POST /v1/rcs/templates/card | Rich card with optional media and 1–4 interactive buttons (reply / open URL / dial call). |
| Carousel | POST /v1/rcs/templates/carousel | 2–10 rich cards rendered as a horizontal carousel. Buttons must be homogeneous across cards. |
All four endpoints persist a template_id you can reuse on every subsequent send.
🔑 Get Your Credentials
- Access your account at areadocliente.liguelead.app.br.
- Navigate to API Credentials.
- Copy your
api-tokenandapp-id.
✏️ Step 1 — Create a template
Text template
POST /v1/rcs/templates/text accepts a JSON body. default_variables is an array of {key, value} pairs — each key is the numeric placeholder (e.g. "1") and value is the fallback rendered when the caller doesn't override the variable at send time.
curl -X POST "https://api.liguelead.com.br/v1/rcs/templates/text" \
-H "api-token: YOUR_API_TOKEN" \
-H "app-id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"title": "OTP Verification",
"body": "Olá {{1}}, seu código é {{2}}",
"default_variables": [
{ "key": "1", "value": "Cliente" },
{ "key": "2", "value": "000000" }
],
"fallback_message": "Seu código LigueLead: 000000"
}'Response
{
"message": "Template created successfully.",
"data": {
"id": "tmpl_2X8…",
"title": "OTP Verification"
}
}Media template
POST /v1/rcs/templates/media accepts application/json. Provide media via either media_url (public URL) or media_file (a base64 data URI) — never both. default_variables is an array of {key, value} pairs.
curl -X POST "https://api.liguelead.com.br/v1/rcs/templates/media" \
-H "api-token: YOUR_API_TOKEN" \
-H "app-id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"title": "Promo banner",
"header": "Oferta de {{1}}",
"body": "Aproveite enquanto durar.",
"media_url": "https://cdn.example.com/banner.jpg",
"default_variables": [{ "key": "1", "value": "Black Friday" }],
"fallback_message": "Promo no nosso site: https://example.com"
}'To upload a local image instead of a URL, send it as a base64 data URI in the media_file field (drop media_url):
-d '{
"title": "Promo banner",
"header": "Oferta",
"body": "Aproveite enquanto durar.",
"media_file": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
}'Card template (interactive buttons)
POST /v1/rcs/templates/card adds a buttons array (1–4 entries) on top of the media contract. Each button is an object with a type discriminator:
type: "reply"+title+ optionalpostback_data— quick-reply button.type: "open_url"+title+url— opens a URL.type: "dial_call"+title+phone_number(E.164) — dials a number.
curl -X POST "https://api.liguelead.com.br/v1/rcs/templates/card" \
-H "api-token: YOUR_API_TOKEN" \
-H "app-id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"title": "Promo Card",
"header": "Olá {{1}}",
"body": "Confira nossa oferta",
"media_url": "https://cdn.example.com/promo.jpg",
"default_variables": [{ "key": "1", "value": "Cliente" }],
"buttons": [
{ "type": "open_url", "title": "Ver oferta", "url": "https://example.com/promo" },
{ "type": "reply", "title": "Quero saber mais", "postback_data": "KNOW_MORE" }
]
}'Carousel template (2–10 cards)
POST /v1/rcs/templates/carousel extends the card contract to multiple cards under a cards array. Each card carries its own header, body, optional media (media_url or base64 data URI media_file), and buttons.
Homogeneity rule: every card must declare the same number of buttons, in the same order of types. The labels and values vary per card, but the structural shape is fixed across the carousel.
curl -X POST "https://api.liguelead.com.br/v1/rcs/templates/carousel" \
-H "api-token: YOUR_API_TOKEN" \
-H "app-id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"title": "Catálogo Promo",
"default_variables": [{ "key": "1", "value": "Cliente" }],
"cards": [
{
"header": "Camiseta {{1}}",
"body": "Tecido premium",
"media_url": "https://cdn.example.com/camiseta.jpg",
"buttons": [
{ "type": "open_url", "title": "Comprar", "url": "https://loja.example.com/camiseta" },
{ "type": "reply", "title": "Mais info", "postback_data": "INFO_CAMISETA" }
]
},
{
"header": "Calça {{1}}",
"body": "Edição limitada",
"media_file": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"buttons": [
{ "type": "open_url", "title": "Comprar", "url": "https://loja.example.com/calca" },
{ "type": "reply", "title": "Mais info", "postback_data": "INFO_CALCA" }
]
}
]
}'Card 0 uses a public media_url while card 1 ships its image inline as a base64 data URI in media_file. The two media options are interchangeable per card.
📤 Step 2 — Send a message
POST /v1/rcs accepts a JSON body and returns 202 Accepted immediately — actual delivery is processed asynchronously and reported back through the configured webhook. Two mutually-exclusive modes are supported:
- Template send: pass
template_id(fromStep 1). Usetemplate_variablesto override the template'sdefault_variablesat send time. - Freeform send: pass
message(capped at 306 characters).
Exactly one of template_id or message must be provided. Devices that don't support RCS receive the SMS fallback automatically — taken from the template's fallback_message for template sends, or from the message field itself for freeform sends.
The 306-character freeform limit is a technical validation rule. Text-message billing is calculated separately by byte size: up to 160 bytes consumes one RCS Basic credit, while text above 160 bytes consumes two RCS Basic credits.
Template send
curl -X POST "https://api.liguelead.com.br/v1/rcs" \
-H "api-token: YOUR_API_TOKEN" \
-H "app-id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"phones": ["11999999999", "+5511888888888"],
"template_id": "tmpl_2X8…",
"template_variables": [
{ "key": "1", "value": "Cliente" },
{ "key": "2", "value": "12345" }
]
}'template_variables follows the same {key, value} array shape used by default_variables on the template-creation endpoints. Keys are numeric strings that match the {{N}} placeholders inside the template body.
Freeform send
curl -X POST "https://api.liguelead.com.br/v1/rcs" \
-H "api-token: YOUR_API_TOKEN" \
-H "app-id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"phones": ["11999999999"],
"message": "Olá! Confira nossa promoção: https://example.com"
}'Response
{
"message": "RCS accepted successfully",
"data": {
"campaign_id": "01J9X…",
"accepted_at": "2026-05-23T18:42:11.234Z"
}
}Persist the campaign_id — every webhook event triggered by this send echoes the same identifier so you can correlate deliveries, reads and postbacks back to the original request.
📡 Step 3 — Receive webhooks
The platform forwards delivery and inbound events to the webhook URL configured for your app-id. There are two event families relevant to RCS:
- Delivery status —
eventis one ofqueued,sent,delivered,read,failed,undelivered. Same payload shape as SMS and voice. - Inbound postback —
event: 'rcs.inbound.postback'fired when a user taps areplybutton. The payload carries thepostback_datayou declared on the template plus the originatingcampaignenvelope so you can correlate the click back to the send.
For the full webhook contract and signature verification, read How to Receive Webhooks.
⚠️ Common pitfalls
- Carousel homogeneity — different button counts or button types across cards return
422. Pick the union of buttons you want and apply it to every card (mismatching only the labels/URLs). media_url×media_fileper card — providing both on the same card (or both for/media//card) returns422.media_fileis a base64 data URI (data:<mime>;base64,...), not a binary upload.phone_numberformat —dial_callbuttons require E.164 (+55…). Numbers without the leading+are rejected.default_variables/template_variablesshape — always an array of{key, value}pairs (on both template creation and send). Sending an object ({"1":"Cliente"}) or a JSON string is rejected.- Numeric keys only — variable keys must match
^\d+$so they line up with{{N}}placeholders. /rcssend mode is mutually exclusive — supply eithertemplate_idormessage, never both and never neither (422).- Freeform
messageis capped at 306 chars — because the same content is reused as the SMS fallback. Template sends are not bound by this cap (their fallback comes from the template you created). This character limit is separate from RCS Basic billing, which uses the 160-byte threshold for text messages. /rcspayload only accepts the documented fields — unknown fields return 422. The SMS fallback is always resolved from the template'sfallback_message(for template sends) or frommessage(for freeform sends); never sent on the request.
🔗 Endpoint reference
GET /v1/rcs/templates— list templates.POST /v1/rcs/templates/text— create a text template.POST /v1/rcs/templates/media— create a media template.POST /v1/rcs/templates/card— create a rich card template.POST /v1/rcs/templates/carousel— create a carousel template.POST /v1/rcs— send an RCS message using atemplate_id.
Updated about 22 hours ago
