Splashify RCS

Docs

Open panel
Panel

Templates

Team-member access: operators can create + delete templates; viewers can list + get but get 403 not_operator on any mutation. Every template is bot-scoped (see bot_id below) and every member sees every template on the account. See the Team members reference.

Templates are the message blueprints you'll later attach to a broadcast. Every template is created against a specific active bot and goes through two verification steps:

  1. Structural validator, runs synchronously at submit time. Rejects the request when the payload violates RCS limits (title length, media size, carousel count, button rules, …). Returns a structured errors[] array so your UI can highlight every issue at once.
  2. Content review, the template is checked against RCS content policy. The verdict lands on ai_review_status, visible on GET /api/v1/rcs/templates/{id}. Allow up to 24 hours; most templates clear sooner, and anything needing a closer look is seen by our team.

Poll a single unified endpoint, GET /api/v1/rcs/templates/{id}/status, for the authoritative approval verdict.

You need an active RCS bot before you can create templates. POST /templates returns HTTP 400 code=no_active_bot if you haven't activated one yet. Apply for a bot at https://rcs.splashifypro.com/apply-rcs.

http
POST   /api/v1/rcs/templates
GET    /api/v1/rcs/templates
GET    /api/v1/rcs/templates/{id}
GET    /api/v1/rcs/templates/{id}/status
DELETE /api/v1/rcs/templates/{id}

Create a template

http
POST /api/v1/rcs/templates
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "bot_id":    "2a4d9a66-7e87-4057-bee7-662879612c21",
  "name":      "order_shipped",
  "category":  "transactional",
  "language":  "en_US",
  "components": {
    "type": "text",
    "text": "Your order #{{1}} has shipped. Track at {{2}}."
  },
  "suggestions": []
}

Variables, {{n}} placeholders

Variables are {{1}}, {{2}}, … up to {{10}}, numbered from {{1}} with no gaps. They may appear in:

  • the message text
  • a card title and description
  • a suggestion's label (text)
  • the end of a suggestion url, e.g. https://shop.com/track/{{2}}. The value is URL-encoded: A 1/2 becomes A%201%2F2 in a path, and after a ? it is encoded as a query value.

They may not appear in a card's media url, its thumbnail, or a dial suggestion's call_to.

The values come from a broadcast's column mapping (see Broadcasts) or from variables on a single send.

Attach a variables array on Create with a label and a sample for each variable:

json
{
  "variables": [
    { "index": 1, "label": "customer_name", "sample": "Rahul" },
    { "index": 2, "label": "order_id",      "sample": "A-1042" }
  ]
}

The label is what the broadcast mapper shows next to each variable. The sample is optional through the API but recommended: with samples, the template is checked with them filled in, so a value that would push the card title past 200 characters is caught now. Without them only the structure is checked. Labels and samples never change what gets sent.

The limits below apply to the text after filling: text 2000 characters, card title 200, card description 2000, suggestion label 25.

A template that breaks a variable rule is refused with HTTP 400:

json
{
  "success": false,
  "code":    "invalid_variables",
  "field":   "variables",
  "message": "Number the variables from {{1}} with no gaps: {{2}} is missing"
}

Other messages you can get:

  • A template can have at most 10 variables, {{1}} to {{10}}
  • A variable in the button 1 link must be at the end of the link (names the field)
  • A variable can't go in the card image or video. Use variables in the text, titles, button labels, or at the end of a link
  • With the sample values filled in: The card title is over 200 characters after filling the variables

Components shape

Three top-level type values are supported:

1. Basic (plain text with optional quick-reply chips)

json
{
  "type": "text",
  "text": "Your order #{{1}} has shipped. Track at {{2}}.",
  "suggestions": [
    { "type": "message", "text": "Track order", "postback": "track_click" }
  ]
}

2. Rich card (single card with media + buttons)

json
{
  "type": "card",
  "card": {
    "title": "Diwali sale, 40% off",
    "description": "Every top brand, ends midnight.",
    "url": "https://cdn.example.com/hero.jpg",
    "suggestions": [
      { "type": "url",  "text": "Shop now", "url": "https://shop.example.com", "postback": "shop_click" },
      { "type": "dial", "text": "Call us",  "call_to": "+919876543210",        "postback": "dial_click" }
    ]
  }
}

3. Carousel (2–10 rich cards)

json
{
  "type": "multiple_cards",
  "cards": [
    { "card": { "title": "Card 1", "description": "…", "url": "https://…", "suggestions": [ … ] } },
    { "card": { "title": "Card 2", "description": "…", "url": "https://…", "suggestions": [ … ] } }
  ]
}

A card can set media_height to SHORT, MEDIUM (the default) or TALL. A card or carousel message can also carry its own suggestions next to card or cards. They show below the cards and follow the same button rules, up to 11.

Button (suggestion) types

Give a location button latitude and longitude where you can. A query alone works only on bots that can show a map search. On other bots that button is left out and the rest of the message still goes, and the validator returns a warning for it.

Limits, the validator enforces all of these

Any violation returns HTTP 400 with an errors[] array. Warnings (recommendations that hurt rendering but don't block sending) come back in warnings[]. They don't stop the submit.

Character limits

Structural limits

Media limits

The panel's upload widget rejects oversize files client-side; the API re-enforces on every submit.

Media URLs must be https:// and publicly reachable, the gateway downloads them at send time; a 404 or private URL fails the send with "Media not accessible".

TTL / expiry

  • ttl and expireTime are mutually exclusive.
  • ttl must end in "s" (e.g. "50s").
  • Both cap at 15 days.

Response, success

json
{
  "success":     true,
  "template_id": "1f7a…",
  "status":      "pending",
  "message":     "Template submitted for review. Most templates are approved within 24 hours."
}

Poll GET /api/v1/rcs/templates/{id}/status for the verdict. Most templates are approved within 24 hours.

Response, no active bot (HTTP 400)

json
{
  "success":  false,
  "code":     "no_active_bot",
  "message":  "Please activate an RCS bot before creating templates. Go to Apply for a bot.",
  "hint_url": "/apply-rcs"
}

Emitted when the caller has no active bot on the account. Direct your users to the apply-rcs flow. A companion bot_not_active code is returned when the target bot exists but hasn't been activated by an admin yet.

Response, validator rejection (HTTP 400)

json
{
  "success":  false,
  "message":  "Template violates RCS limits. Fix the issues below and resubmit",
  "errors": [
    "Carousel card 3: title is 217 characters, max 200",
    "Carousel card 3 → button 1: label is 34 characters, max 25",
    "Carousel card 5: media URL must start with https://"
  ],
  "warnings": [
    "Rich card: 3 buttons. We recommend ≤ 2 per card to reduce cropping"
  ]
}

Get one template

http
GET /api/v1/rcs/templates/{id}
json
{
  "success": true,
  "template": {
    "template_id":      "1f7a…",
    "bot_id":           "2a4d9a66-7e87-4057-bee7-662879612c21",
    "provider":         "jiocx",
    "name":             "order_shipped",
    "category":         "transactional",
    "language":         "en_US",
    "components":       { "type": "text", "text": "Hi {{1}}, your order {{2}} shipped." },
    "suggestions":      [ ],
    "variables": [
      { "index": 1, "label": "customer_name", "sample": "Rahul" },
      { "index": 2, "label": "order_id",      "sample": "" }
    ],
    "ai_review_status": "approved",
    "ai_review_reason": "Clean transactional template.",
    "effective_status": "approved",
    "submitted_at":     "2026-05-27T08:31:21Z",
    "reviewed_at":      "2026-05-27T08:31:24Z"
  }
}

effective_status is what broadcasts check. ai_review_status carries the content-review verdict, and provider is the gateway resolved from the template's bot.

variables lists every variable the content uses, in order, even one created without a label: label and sample are empty strings when they were not set. A template with no variables returns an empty list.

List templates

http
GET /api/v1/rcs/templates

Returns { "success": true, "templates": [ … ] } with the same row shape as GET /…/{id}, ordered by submission time desc.

Check template approval status

One unified endpoint returns the authoritative approval verdict for every template.

http
GET /api/v1/rcs/templates/{id}/status
Authorization: Bearer <jwt>

Response:

json
{
  "success":    true,
  "status":     "approved",
  "ai_status":  "approved",
  "reason":     "Clean transactional template.",
  "score":      0.94,
  "checked_at": "2026-07-23T14:03:22Z"
}
  • status, the human-readable verdict (pending, approved, rejected, …). This mirrors effective_status on the standard Get and is what broadcasts check.
  • ai_status, the content-review verdict for the template.
  • reason / score, the justification and a confidence score.
  • checked_at, when the verdict was last computed.

Error cases:

Delete

http
DELETE /api/v1/rcs/templates/{id}

Removes the template. Broadcasts already running with this template keep their assignment and won't break.

Email notifications

Every template state transition emails the account owner:

  • Template submitted → "Template in review"
  • Approved → "Template approved"
  • Rejected → "Template needs changes" (with reason)

Test send

Preview a template on your own phone before submitting for review. The send never touches your wallet, never counts against your broadcasts, and never triggers billing.

Endpoint

http
POST /api/v1/rcs/test-send
{
  "bot_id":      "<uuid>",
  "template_id": "<uuid>"   // OR provide "message": <raw message content>
}

Either template_id (from an existing saved template, approved or not) or an inline message (the raw message content JSON) is required. The server renders template_id the same way a broadcast send does.

Recipient

Always me.test_phone_number || me.phone. There is no to field. The composer button reads back the phone in the label so the operator can confirm where it lands (Send test to +91987…321).

Set the override on PATCH /api/v1/rcs/settings/test-phone:

http
PATCH /api/v1/rcs/settings/test-phone
{ "test_phone_number": "+919876543210" }

Sending { "test_phone_number": "" } clears the override and the account phone is used instead.

Response

json
{
  "success": true,
  "test_send_id": "018e5f01-…",
  "msg_id":       "018e5f01-…",
  "phone":        "+919876543210"
}

History

http
GET /api/v1/rcs/test-sends?limit=20
json
{
  "success": true,
  "test_sends": [
    {
      "test_send_id":   "018e5f01-…",
      "bot_id":         "…",
      "template_id":    "…",
      "phone":          "+919876543210",
      "msg_id":         "…",
      "status":         "delivered",
      "sent_at":        "2026-07-14T09:12:03Z",
      "delivered_at":   "2026-07-14T09:12:04Z",
      "read_at":        "0001-01-01T00:00:00Z",
      "failure_reason": ""
    }
  ]
}

Real-time status

Status transitions arrive on the panel event stream. Consumers should key off test_send_id (matches the POST /test-send response).

json
{
  "type": "test_send.status",
  "data": {
    "test_send_id":   "018e5f01-…",
    "status":         "delivered",
    "failure_reason": ""
  }
}

Status values: queued, sent, delivered, read, failed.

Rules

  • Bot must be in the caller's scope AND active AND provisioned (agent id + agent key). See the errors reference for bot_not_active / bot_not_provisioned.
  • Test sends bypass the account's opt-out list, the target is the operator themselves.
  • Never billed. applyPanelSuccessBilling skips any msg_id that resolves to a rcs_test_sends_by_msg row. Prepaid wallets aren't debited; postpaid usage counters aren't bumped.
  • Viewers get 403 not_operator.