# Build RCS rich cards and carousels

Use one card for an appointment or order and a carousel for a small set of choices. Start with the [first-message walkthrough](/docs/get-started/send-your-first-rcs-message), which supplies the channel, recipient and runnable HTTP clients used here.

## Build a card

Bird's [RCS sending reference](/docs/engagement-platform/api/channels-api/supported-channels/programmable-rcs/sending-messages) maps both a single rich card and a carousel to `body.type: "carousel"`. One item produces a single card. The title and description explain the task; `actions` provide its next steps.

| Element        | Appointment example               | Customer check                                                  |
| -------------- | --------------------------------- | --------------------------------------------------------------- |
| Title          | Your studio visit                 | Understandable without an image.                                |
| Description    | Tuesday, 10:00. Riverside Studio. | Actual date, time zone and location are clear.                  |
| URL action     | View appointment                  | Opens this appointment after sign-in.                           |
| Postback       | Change my time                    | Returns a stable value to your rescheduling handler.            |
| Optional media | Studio entrance                   | Helps recognize the location; essential details remain in text. |

Add an accessible HTTPS image through the item's `mediaUrl` and describe it with `altText`. Keep its URL reachable when the platform fetches it. This is a `body` example to place inside the request's existing receiver/reference envelope:

```json
{
  "type": "carousel",
  "carousel": {
    "items": [
      {
        "title": "Your studio visit",
        "description": "Tuesday, 10:00. Riverside Studio.",
        "mediaUrl": "https://your-app.example/images/studio-entrance.jpg",
        "altText": "Riverside Studio entrance",
        "actions": [
          {
            "type": "link",
            "link": {
              "text": "View appointment",
              "url": "https://your-app.example/appointments/demo"
            }
          }
        ]
      }
    ]
  }
}
```

Replace both example URLs with your own resources. See the [illustrative card experience](/rcs-business-messaging#experience) for the visual sequence; verify the actual rendering on your test phone.

## Choose an action or a reply

A `link` opens an application destination. A `reply` displays suggested text. A `postback` includes both a visible `text` and a stable `payload` you choose. Use an opaque action token in a real booking workflow; keep the authorized customer and current appointment in your application's records.

For the quickstart's **Change my time** button, the [interaction API](/docs/engagement-platform/api/channels-api/supported-channels/programmable-rcs/message-interactions) returns `type: "clicked"` and `metadata.button.payload: "appointment-demo/change"`. The inbound message represents the selection in `body.text.actions[].postback.payload`. Button position in `details` helps debug rendering; the payload identifies the application intent.

This application-owned example consumes `rcs-interactions.json` from the quickstart and records one rescheduling request in a local SQLite database. Save it as `record_rcs_choice.py` and run `python3 record_rcs_choice.py`. It does not change a booking or send another message.

```python
import json
import os
import sqlite3
from pathlib import Path

sent = json.loads(Path("rcs-result.json").read_text())
interactions = json.loads(Path("rcs-interactions.json").read_text())["results"]
with sqlite3.connect("rcs-choices.sqlite") as db:
    db.execute("CREATE TABLE IF NOT EXISTS seen (interaction_id TEXT PRIMARY KEY)")
    db.execute("""CREATE TABLE IF NOT EXISTS tasks (
        task_key TEXT PRIMARY KEY, message_id TEXT NOT NULL, status TEXT NOT NULL
    )""")
    for event in interactions:
        if event.get("channelId") != os.environ["RCS_CHANNEL_ID"]:
            continue
        if event.get("messageId") != sent["id"] or event.get("type") != "clicked":
            continue
        payload = (event.get("metadata") or {}).get("button", {}).get("payload")
        if payload != "appointment-demo/change" or not event.get("id"):
            continue
        with db:
            inserted = db.execute(
                "INSERT OR IGNORE INTO seen VALUES (?)", (event["id"],)
            ).rowcount
            if inserted:
                db.execute(
                    "INSERT OR IGNORE INTO tasks VALUES (?, ?, ?)",
                    ("appointment-demo/reschedule", sent["id"], "requested"),
                )
    print(db.execute("SELECT task_key, status FROM tasks").fetchall())
```

Repeated reads of the same interactions leave one task. Two distinct clicks for this same appointment also leave one task. In your application, derive that task key from the saved appointment and action, check the customer's authority and current booking state, then let the booking service offer available times. A reply requests a change; it does not prove a new time was reserved.

For live replies, create a [webhook subscription](/docs/engagement-platform/api/notifications-api/api-reference/webhook-subscriptions/create-a-webhook-subscription) for `service: "channels"`, `event: "rcs-google.inbound"`, filtered by `channelId`. Verify the [webhook signature](/docs/engagement-platform/api/notifications-api/api-reference/webhook-subscriptions/verifying-a-webhook-subscription) before consuming its message. Deduplicate inbound message IDs in your receiving handler. Choose either the inbound selection or the corresponding click as the source of the business task so both do not trigger it. Store ordinary free-text replies as conversation input; route unknown payloads for review instead of treating them as a booking instruction.

## Add a carousel

Replace the quickstart's `body` with two comparable appointment options. Each choice has its own payload; both belong to the same appointment workflow.

```json
{
  "type": "carousel",
  "carousel": {
    "items": [
      {
        "title": "Morning visit",
        "description": "Tuesday, 10:00. Riverside Studio.",
        "actions": [
          {
            "type": "postback",
            "postback": { "text": "Choose morning", "payload": "appointment-demo/morning" }
          }
        ]
      },
      {
        "title": "Afternoon visit",
        "description": "Tuesday, 15:00. Riverside Studio.",
        "actions": [
          {
            "type": "postback",
            "postback": { "text": "Choose afternoon", "payload": "appointment-demo/afternoon" }
          }
        ]
      }
    ]
  }
}
```

Extend the application's allowlist to these two choices and check availability when processing the selection. If the slot has gone, offer fresh options. The quickstart's single-choice SQLite example deliberately ignores these different payloads until you implement that behavior.

## Preview and publish

For reusable content, follow [Create an RCS message template](/docs/engagement-platform/guides/channels/channels/supported-channels/google-rcs/create-a-google-rcs-message-template). Add variables and language versions, preview resolved content, and publish. Use [cross-platform optimization](/docs/engagement-platform/guides/channels/channels/supported-channels/google-rcs/rcs-cross-platform-optimization) and its Android/iOS previews to check layout and button compatibility. Recheck image crop, long values and action destinations on representative phones.

A template send replaces `body` with `template` in the direct-channel request. Use the published project's ID, version and locale. This example assumes your template declares a string parameter called `customer_name`:

```json
{
  "projectId": "replace-with-template-project-id",
  "version": "replace-with-published-version",
  "locale": "en",
  "parameters": [{ "type": "string", "key": "customer_name", "value": "Alex" }]
}
```

Keep the template's content and variables aligned with the action handler. Updating display text should not accidentally change what an existing action token means.

## Continue building

- [Send your first RCS message](/docs/get-started/send-your-first-rcs-message)
- [Configure SMS fallback](/docs/guides/rcs/capability-and-fallback)
- [Trace a delivery or reply problem](/docs/guides/rcs/troubleshooting)
- [RCS resource library](/rcs/resources)

## Related resources

- [What Is RCS for Business?](/explained/rcs/what-is-rcs-messaging) (answer)

[Get an implementation brief](/learn/workspace?topic=rcs)
