# RCS

Register a verified RCS agent, send rich cards, carousels and buttons to Android and iPhone, fall back to SMS, and receive replies and receipts in the inbox.

RCS is rich messaging for phones: Android users get it in Google Messages, and iPhone users in Messages on iOS 18 and later. Your messages arrive under a checked business identity with your name, logo, banner and a verified badge, not from an anonymous number. They can carry cards, carousels and buttons, and you see when they are read. In Pozy you register an RCS agent, then send with `POST /v1/rcs/messages`. Replies land in the [Inbox](https://pozy.ai/docs/inbox) with the platform id `rcs`.

## What you can do

| Feature | Supported | Where |
| --- | --- | --- |
| Register a brand and agent with the carriers | Yes, through the API | [Connect](#connect) |
| Test on your own phones before launch | Yes (US) | [Connect](#connect) |
| Text with buttons | Yes | [Sending messages](#sending-messages) |
| Media, rich cards, carousels | Yes | [Sending messages](#sending-messages) |
| SMS fallback for phones without RCS | Yes, with `smsFallbackFrom` | [SMS fallback](#sms-fallback) |
| Check which numbers can receive RCS | Up to 100 at a time | [SMS fallback](#sms-fallback) |
| Replies, button taps, locations, files | Yes | [Inbox](#inbox) |
| Delivery and read receipts | Yes | [Inbox](#inbox) |
| Publishing, analytics | No | Messaging only |

## Connect

Carriers approve two things before an agent can send:

- **Brand**: the company that is legally responsible for what is sent, with its registered name, tax id, address and a contact person. Several agents can share one brand.
- **Agent**: the sender identity on the phone, with a display name, description, logo, banner, colour and contact buttons. An agent sits in one profile, which gets an inbox account for it.

The workspace needs a plan with the inbox, usage-based billing and a saved card. Pozy reviews every request first; carriers see nothing and you pay nothing before that review is done.

### Upload the logo and banner

The carriers' sizes are strict: the logo is 224 x 224 and at most 50 KB, the banner 1440 x 448 and at most 200 KB. You do not have to hit them yourself. Send a PNG, JPEG or WebP to [Upload an RCS logo or banner](https://pozy.ai/docs/api/uploadRcsAsset) with `kind: "logo"` or `kind: "banner"`; Pozy crops and compresses it and returns the URL to use.

### Request the agent

Then call [Request an RCS agent](https://pozy.ai/docs/api/createRcsAgent). Send the profile, the agent's details, and the brand: a full `brand` object for a new one, or the `brandId` of an earlier one, but not both. A profile can have only one agent in progress or live at a time.

```json
{
  "profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
  "displayName": "Acme Deliveries",
  "useCase": "TRANSACTIONAL",
  "brand": {
    "displayName": "Acme",
    "legalName": "Acme Logistics LLC",
    "legalEntityType": "LIMITED_LIABILITY_COMPANY",
    "organizationType": "PRIVATE_PROFIT",
    "websiteUrl": "https://acme.example.com",
    "taxId": "12-3456789",
    "address": { "line1": "100 Market St", "city": "San Francisco", "state": "CA", "postalCode": "94105", "country": "US" },
    "contact": { "firstName": "Dana", "lastName": "Lee", "email": "dana@acme.example.com", "phone": "+14155550100" }
  },
  "profile": {
    "description": "Delivery updates from Acme",
    "logoUrl": "https://pozy.ai/media/acme-logo.png",
    "heroUrl": "https://pozy.ai/media/acme-banner.png",
    "brandColor": "#1A3D8F",
    "privacyPolicyUrl": "https://acme.example.com/privacy",
    "termsUrl": "https://acme.example.com/terms",
    "phone": { "number": "+14155550100", "label": "Call us" }
  }
}
```

### Fix anything the review flags

If something needs changing, the agent moves to `changes_requested` with a note in `reviewNote`. Send only the changed fields with [Update an RCS agent](https://pozy.ai/docs/api/updateRcsAgent) and it goes back into review. Once details are filed with the carriers they are locked; only `smsFallbackFrom` can still change.

### Test on your phones (US)

When a US agent reaches `testing`, it can message phones you invite, and no one else. Invite one with [Invite an RCS test phone](https://pozy.ai/docs/api/addRcsTestDevice) and accept the invite in that phone's messaging app. T-Mobile and AT&T numbers cannot be test phones.

### Send the launch details

[Send the launch filing](https://pozy.ai/docs/api/requestRcsAgentLaunch) tells the carriers what the agent will send and how people agree to it. It needs three or more `messageExamples`; the `consent` block, with how people opt in and the replies to opt-in, HELP and opt-out (for an opt-in on a website, add the page in `callToActionUrl` and a screenshot of it in `callToActionMediaUrl`); and `testVideoUrl`, a public or unlisted recording of a phone texting START, STOP and HELP to the agent and having one real exchange. For US agents, file it after the agent works on your test phones. In other countries, file it straight after the request.

Follow progress with the `rcs.agent.status_updated` webhook rather than polling.

| Market | Status path | Typical time |
| --- | --- | --- |
| United States | `requested`, `brand_vetting`, `agent_review`, `testing`, `launch_review`, `launching`, `live` | 4 to 6 weeks of carrier review |
| Other markets | `requested`, `brand_vetting`, `launching`, `live` | Varies by carrier |

Side exits are `changes_requested`, `rejected` (with `declineReason`) and `deactivated`. The launch market is the country of the brand's address. Supported countries include the US, UK, Austria, Bangladesh, Brazil, DR Congo, France, Germany, India, Italy, Mexico, the Netherlands, Nigeria, Norway, Portugal, Singapore, South Africa, Spain and Sweden. Spanish carriers have paused new launches for now; Spanish agents are filed once they reopen.

## Sending messages

You can send once the agent is `testing` (to accepted test phones) or `live` (to any RCS phone). Pass `text` for a plain message or `content` for anything richer.

```bash cURL
curl -X POST https://api.pozy.ai/v1/rcs/messages \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: delivery-A10423-out" \
  -d '{
    "agentId": "66c4d2e1f0a9b8c7d6e5f4a3",
    "to": "+13105551234",
    "content": {
      "type": "text",
      "text": "Your parcel A-10423 is out for delivery today.",
      "suggestions": [
        { "type": "reply", "text": "Track it", "postbackData": "track:A-10423" },
        { "type": "openUrl", "text": "Change delivery", "url": "https://acme.example.com/d/A-10423" },
        { "type": "dial", "text": "Call driver", "phoneNumber": "+14155550142" }
      ]
    },
    "fallbackText": "Your parcel A-10423 is out for delivery today. Track: https://acme.example.com/d/A-10423"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/rcs/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": "delivery-A10423-out",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agentId: "66c4d2e1f0a9b8c7d6e5f4a3",
    to: "+13105551234",
    content: {
      type: "text",
      text: "Your parcel A-10423 is out for delivery today.",
      suggestions: [
        { type: "reply", text: "Track it", postbackData: "track:A-10423" },
        { type: "openUrl", text: "Change delivery", url: "https://acme.example.com/d/A-10423" },
        { type: "dial", text: "Call driver", phoneNumber: "+14155550142" },
      ],
    },
    fallbackText: "Your parcel A-10423 is out for delivery today. Track: https://acme.example.com/d/A-10423",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.id, data.conversationId);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/rcs/messages",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": "delivery-A10423-out",
    },
    json={
        "agentId": "66c4d2e1f0a9b8c7d6e5f4a3",
        "to": "+13105551234",
        "content": {
            "type": "text",
            "text": "Your parcel A-10423 is out for delivery today.",
            "suggestions": [
                {"type": "reply", "text": "Track it", "postbackData": "track:A-10423"},
                {"type": "openUrl", "text": "Change delivery", "url": "https://acme.example.com/d/A-10423"},
                {"type": "dial", "text": "Call driver", "phoneNumber": "+14155550142"},
            ],
        },
        "fallbackText": "Your parcel A-10423 is out for delivery today. Track: https://acme.example.com/d/A-10423",
    },
)
print(resp.json())
```

`content.type` picks the layout. Every type takes up to 11 `suggestions` (buttons) under the message.

| `content.type` | What it sends | Options |
| --- | --- | --- |
| `text` | Text, up to 3,072 characters | `suggestions` |
| `media` | An image or video on its own | `media.url`, `thumbnailUrl`, `height` (`SHORT`, `MEDIUM`, `TALL`) |
| `card` | Media, a title (200 characters), a description (2,000) and up to 4 buttons on the card | `orientation` (`VERTICAL`, `HORIZONTAL`), `thumbnailAlignment` |
| `carousel` | 2 to 10 cards to swipe through | `cardWidth` (`SMALL`, `MEDIUM`) |

| Button `type` | On tap | Also send |
| --- | --- | --- |
| `reply` | The person replies with the button's label | `postbackData` |
| `openUrl` | A web page opens | `url`; optionally `application` (`BROWSER` or `WEBVIEW`) and `webviewViewMode` |
| `dial` | Starts a call | `phoneNumber` in E.164 |
| `viewLocation` | Opens a map pin | `latitude`, `longitude`, optional `label` |
| `shareLocation` | The phone offers to send its location | Nothing |
| `calendarEvent` | The phone offers to save an event | `startTime`, `endTime`, `title` (100 characters); optionally `description` (500) |

Keep labels to 25 characters. `postbackData` is your own string of up to 2,048 characters (JSON works) that Pozy hands back as is on a tap; without it you get the label. Set `ttlSeconds` if a message that has not been delivered by then should be dropped.

### SMS fallback

Set `smsFallbackFrom` on the agent to one of your numbers that can text, and phones without RCS still get the message, as an SMS from that number. The SMS body is your `fallbackText` if you send one; if not, Pozy uses the message text, or for a card its title, description and media link. With no fallback number, phones without RCS get nothing.

To check ahead of time, call [Check RCS capability](https://pozy.ai/docs/api/getRcsCapabilities) with up to 100 numbers. Each result has `rcsCapable` and the rich `features` the phone supports.

## Media requirements

| Item | Requirement |
| --- | --- |
| Media | A public URL for the image or video, with an optional `thumbnailUrl` |
| Agent logo | 224 x 224 px, under 50 KB (uploads are cropped and compressed to fit) |
| Agent banner | 1440 x 448 px, under 200 KB |
| Brand colour | A six-digit hex colour that reaches a 4.5:1 contrast ratio against white |

## Limits

| Limit | Value |
| --- | --- |
| Buttons | 11 per message, labels up to 25 characters |
| Card | Title 200 characters, description 2,000, 4 buttons |
| Carousel | 2 to 10 cards |
| Capability check | 100 numbers per call |
| Agent profile | `displayName` up to 40 characters, `description` up to 100 |
| Agents | One open agent per profile |

A few things are refused. You cannot send while the agent is in neither `testing` nor `live`. The brand contact cannot be a Gmail address or a generic `info@` or `support@` mailbox. The contact buttons need more than a website. Replies sent from the inbox are plain RCS text only; for cards and buttons, use `POST /v1/rcs/messages`.

## Analytics

RCS has no post analytics. Each message reports delivery and reads as webhooks, and conversation metrics are in [Inbox analytics](https://pozy.ai/docs/analytics/inbox-analytics).

## Inbox

Replies thread into the agent's inbox conversation, keyed by the person's phone number, with `platform: "rcs"`. Every inbound message fires `message.received`; the first message of a new thread also fires `conversation.started`.

| Incoming | Where it shows up |
| --- | --- |
| A text | `text` |
| A tapped `reply` button | The label in both `text` and `metadata.postbackTitle`, and the button's `postbackData` in `metadata.postbackPayload` |
| A shared location | `latitude` and `longitude` under `metadata.location` |
| A photo, video or file | `attachments`, stored by Pozy |

Match on `postbackPayload`, not the label: labels are for people and may change. Your sends report `message.delivered`, `message.read` and `message.failed`, including messages that went out through the SMS fallback. STOP (and STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT) opts the person out and later sends return `409`. Only their START opts them back in.

## Pricing

An RCS agent has a setup fee when it is submitted, a one-time fee on US carrier approval, and a monthly fee from the day it goes live (never while it is in review). Messages are billed per message, sent and received, by destination. See [Pricing](https://pozy.ai/docs/billing) for the current amounts.

## Errors you may see

| Status | Code | Why | What to do |
| --- | --- | --- | --- |
| `403` | | Before launch, only test phones that accepted the invite can receive | Invite that phone as a tester, or wait until the agent is `live` |
| `403` | | Your plan has no inbox | Upgrade to a plan that includes it |
| `409` | `recipient_opted_out` | The person texted STOP | Only their START lets you message them again |
| `409` | `invalid_resource_state` | Carriers have not created the agent yet | Retry once it reaches `testing` or `live` |
| `409` | | A reused brand was rejected, or the change is no longer allowed | Send a new `brand`. Once filed, only `smsFallbackFrom` can change |
| `404` | | The agent is not in your workspace | Check `agentId` |
| `422` | `USAGE_BILLING_REQUIRED` | Billing is not usage-based | Turn on usage-based billing |
| `402` | | No card on file | Add a card |

[Errors](https://pozy.ai/docs/getting-started/errors) covers the error format.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/rcs/brands`](https://pozy.ai/docs/api/listRcsBrands) | List RCS brands |
| [`GET /v1/rcs/agents`](https://pozy.ai/docs/api/listRcsAgents) | List RCS agents |
| [`POST /v1/rcs/agents`](https://pozy.ai/docs/api/createRcsAgent) | Request an RCS agent |
| [`GET /v1/rcs/agents/{agentId}`](https://pozy.ai/docs/api/getRcsAgent) | Get an RCS agent |
| [`PATCH /v1/rcs/agents/{agentId}`](https://pozy.ai/docs/api/updateRcsAgent) | Update an RCS agent |
| [`DELETE /v1/rcs/agents/{agentId}`](https://pozy.ai/docs/api/deactivateRcsAgent) | Deactivate an RCS agent |
| [`POST /v1/rcs/agents/{agentId}/launch-request`](https://pozy.ai/docs/api/requestRcsAgentLaunch) | Send the launch filing |
| [`GET /v1/rcs/agents/{agentId}/test-devices`](https://pozy.ai/docs/api/listRcsTestDevices) | List RCS test phones |
| [`POST /v1/rcs/agents/{agentId}/test-devices`](https://pozy.ai/docs/api/addRcsTestDevice) | Invite an RCS test phone |
| [`DELETE /v1/rcs/agents/{agentId}/test-devices/{testDeviceId}`](https://pozy.ai/docs/api/removeRcsTestDevice) | Remove an RCS test phone |
| [`POST /v1/rcs/assets`](https://pozy.ai/docs/api/uploadRcsAsset) | Upload an RCS logo or banner |
| [`POST /v1/rcs/messages`](https://pozy.ai/docs/api/sendRcsMessage) | Send an RCS message |
| [`GET /v1/rcs/capabilities`](https://pozy.ai/docs/api/getRcsCapabilities) | Check RCS capability |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`message.received`](https://pozy.ai/docs/webhooks/events#message-received) | Message received. |
| [`conversation.started`](https://pozy.ai/docs/webhooks/events#conversation-started) | Conversation started. |
| [`message.delivered`](https://pozy.ai/docs/webhooks/events#message-delivered) | Message delivered. |
| [`message.read`](https://pozy.ai/docs/webhooks/events#message-read) | Message read. |
| [`message.failed`](https://pozy.ai/docs/webhooks/events#message-failed) | Message delivery failed. |
| [`rcs.agent.status_updated`](https://pozy.ai/docs/webhooks/events#rcs-agent-status-updated) | RCS agent status updated. |
