---
title: "Open a conversation and send the first message"
method: POST
path: "/v1/conversations"
tags: ["Conversations"]
---

# Open a conversation and send the first message

`POST /v1/conversations`

## Request

- Base URL: `https://api.cadencercs.com`
- URL: `https://api.cadencercs.com/v1/conversations`
- Auth: HTTP bearer

Sandbox keys simulate delivery — nothing reaches a phone — so this is safe to call with fake E.164 numbers while you build.

**To get two-way messaging without keeping a process alive, attach a `flow`.** Cadence then answers every reply on its own inbound webhook, so a recipient who replies an hour later, after your deploy, still gets the next step. Without one, nothing is answered for you: you drive the conversation yourself over `GET /v1/events`.

`optin` is required and records the consent provenance you are asserting.

Three fields worth knowing about, all optional and none obvious: **`idempotencyKey`** makes a retry safe — without it a cron that retries double-sends and double-bills, which is the single most expensive omission on this route. **`turnCeiling`** caps how long a conversation may run (default 30). **`phiSafe`** enforces PHI-safe-minimal content for clinical use.

## Request body

- OpenConversationRequest: Open a conversation and send the first message. `opening` is required **unless** you attach a `flow`, in which case the flow's first step is the opening and the server sends it — passing a different opening returns 422 `OPENING_IS_STEP_ONE` rather than asking the recipient nothing and then filing their reply against a step they never saw.
  - `agentId` string, required
  - `brandId` string, required
  - `to` string, required
  - `useCase` 'transactional' | 'promo' | 'conversational' | 'leadgen', required
  - `opening` Opening
    - `text` string: ≤3072 characters.
    - `disclaimer` string: Frequency, rates, HELP and STOP — appended to this message and this message only, on both the RCS text and the SMS fallback so the two cannot drift apart. `flow` has always had this; without it, a webhook-driven conversation had to paste the carrier disclosure into two fields by hand. Carriers expect it at the start of a programme.
    - `richCard` object: A Google RBM rich card, passed through untouched. Prefer `card` or `carousel`: chips inside a hand-rolled `richCard` are not numbered by us, so taps on them arrive with no `optionLabel` and cannot be attributed to a question. A message carries either `richCard` or `text`, never both.
    - `card` object: A single rich card, built for you. Cannot be combined with `text`, `media`, `carousel` or `richCard` — RBM's contentMessage holds exactly one of them (`MULTIPLE_MESSAGE_BODIES`); the card's own `title` and `description` carry the words.
      - `title` string: ≤200 characters, but a MEDIUM carousel card is 232 DP wide, so anything past a short phrase truncates rather than wraps.
      - `description` string: ≤2000 characters.
      - `media` object
        - `url` string, uri, required: https only — Google fetches it themselves.
        - `altText` string, required: What the file says, for anyone who cannot see it. RBM has nowhere to put this, so it does not travel to Google — it is what the SMS leg carries instead of the image.
        - `thumbnailUrl` string, uri: Max 100 kB; 50 kB or less is better.
        - `height` 'SHORT' | 'MEDIUM' | 'TALL': 112, 168 or 264 DP. Google recommends a different aspect ratio for each (7:2, 21:9, 3:2), so an image cropped for one height is cropped wrong for another. Defaults to MEDIUM: TALL pushes the text below the fold on smaller handsets.
      - `suggestions` union[]: At most 4 — a per-card limit, separate from and much smaller than the 11-chip list below the message. Taps on these resolve to `optionLabel` exactly like the message-level chips, because their postback data is numbered in the same sequence.
        - union
          - string: A reply chip.
          - object: An action chip: opens a URL, dials, adds a calendar event, or shows a location.
            - `text` string, required
            - `action` union, required
              - …
            - `smsFallback` string: What the SMS leg says instead. Defaulted per action type; "" says nothing.
      - `orientation` 'VERTICAL' | 'HORIZONTAL': HORIZONTAL puts the image beside the text instead of above it. A horizontal card carrying media must also carry a title, description or suggestions (`HORIZONTAL_CARD_NEEDS_CONTENT`) — an image alone has nothing to sit beside.
      - `thumbnailImageAlignment` 'LEFT' | 'RIGHT': Which side the image sits on. Only meaningful with HORIZONTAL.
    - `carousel` object: A swipeable row of cards. Always vertical — there is no orientation to choose. The whole payload must serialise under 250 KB (`RICHCARD_TOO_LARGE`), which ten 2000-character descriptions can approach; image URLs cost only their length.
      - `width` 'SMALL' | 'MEDIUM': 120 or 232 DP. Defaults to MEDIUM.
      - `cards` object[], required: 2 to 10. One card in a carousel is a standalone card drawn worse, and Google refuses it.
        - `title` string: ≤200 characters, but a MEDIUM carousel card is 232 DP wide, so anything past a short phrase truncates rather than wraps.
        - `description` string: ≤2000 characters.
        - `media` object
          - `url` string, uri, required: https only — Google fetches it themselves.
          - `altText` string, required: What the file says, for anyone who cannot see it. RBM has nowhere to put this, so it does not travel to Google — it is what the SMS leg carries instead of the image.
          - `thumbnailUrl` string, uri: Max 100 kB; 50 kB or less is better.
          - `height` 'SHORT' | 'MEDIUM' | 'TALL': 112, 168 or 264 DP. Google recommends a different aspect ratio for each (7:2, 21:9, 3:2), so an image cropped for one height is cropped wrong for another. Defaults to MEDIUM: TALL pushes the text below the fold on smaller handsets.
        - `suggestions` union[]: At most 4 — a per-card limit, separate from and much smaller than the 11-chip list below the message. Taps on these resolve to `optionLabel` exactly like the message-level chips, because their postback data is numbered in the same sequence.
          - union
            - string: A reply chip.
            - object: An action chip: opens a URL, dials, adds a calendar event, or shows a location.
              - …
    - `suggestions` union[]: Chips, at most 11. A string is a reply chip; an object is an action chip (open a URL, dial, calendar, location). Action chips are excluded from SMS numbering and carry no `optionIndex`.
      - union
        - string: A reply chip.
        - object: An action chip: opens a URL, dials, adds a calendar event, or shows a location.
          - `text` string, required
          - `action` union, required
            - object
              - …
            - object
              - …
            - object
              - …
            - object
              - …
            - object
              - …
          - `smsFallback` string: What the SMS leg says instead. Defaulted per action type; "" says nothing.
    - `media` object: An image, chart, PDF or short video to send with this message. Google fetches the URL from their own infrastructure, so it must be publicly reachable over HTTPS — anything behind a session, a VPN or an expired signed URL fetches as a 403 and the message arrives empty. Cannot be combined with `richCard` (`MEDIA_AND_RICHCARD`); `media` builds the card for you when there is text to carry.
      - `url` string, uri, required: https only.
      - `altText` string, required: What the file says, for anyone who cannot see it. Required — RCS renders media inline, and on the SMS leg this text is all the recipient gets.
      - `thumbnailUrl` string, uri: A still for video or PDF. Google generates one when omitted, where it can.
    - `expiresIn` integer: Seconds this message stays worth delivering. RBM holds a message for an offline handset up to 30 days and delivers it whenever the device returns — for anything tied to a moment that is a data problem, not a cosmetic one: a daily check-in that lands three days late has its answer recorded against the wrong day, and nothing downstream can tell. On the SMS leg this becomes the carrier validity period, which our SMS provider caps at 10 hours, so a longer deadline is exact on RCS and clamped on SMS.
    - `expiresAt` string: An absolute deadline instead of a duration. Send one or the other, never both (`409 CONFLICTING_EXPIRY`). Already in the past is refused (`EXPIRY_IN_PAST`) rather than sent — a message that succeeds and is never delivered is the loudest possible silence.
    - `smsFallbackText` string: ≤1600 characters. **Required whenever `suggestions` or `richCard` is present** — omitting it is `400 MISSING_SMS_FALLBACK`, not a generated default. This said "generated from suggestions when omitted", which was true of a branch the validator makes unreachable. Any action chips' content is appended to what you write here, unless your text already contains it.
  - `rcsOnly` boolean: Disable SMS delivery for the entire conversation. Requires a backend that guarantees RCS-only sends. Defaults to false.
  - `optin` object, required
    - `method` string, required
    - `evidenceRef` string
    - `capturedAt` string, required
  - `phiSafe` boolean
  - `turnCeiling` integer
  - `idempotencyKey` string
  - `flow` Flow: **This is how you get two-way messaging without keeping a process alive.** Attach a flow when you open a conversation and Cadence answers every reply itself, on its own inbound webhook from Google — a recipient who replies an hour later, after your deploy or your restart, still gets the next step. Read the results from `GET /v1/conversations/{id}/answers`. It is the whole definition rather than an id on purpose: editing a flow mid-wave must not change the steps already in flight for someone halfway through it. **A flow is a fixed script with one level of branching** (`followUps`), not a place to put a model. If your own model writes each reply, register a delivery webhook instead and answer with `POST /v1/conversations/{id}/messages`. Called `flow`, not `survey` — appointment confirmations, onboarding sequences and lead qualification are the same machine.
    - `id` string, required
    - `name` string, required
    - `steps` FlowStep[], required: In order. `steps[0]` is the opening message.
      - `id` string, required: Your identifier for this step. Answers are keyed by it in `GET /v1/conversations/{id}/answers`.
      - `text` string, required: What is asked. Required.
      - `suggestions` union[]: Chip labels, ≤11. RCS renders tap targets; SMS gets a numbered fallback automatically.
        - union
          - string: A reply chip.
          - object: An action chip: opens a URL, dials, adds a calendar event, or shows a location.
            - `text` string, required
            - `action` union, required
              - …
            - `smsFallback` string: What the SMS leg says instead. Defaulted per action type; "" says nothing.
      - `smsFallbackText` string: Overrides the generated SMS text for this step.
      - `followUps` FlowFollowUp[]: Asked only when the answer matches. Depth is capped.
        - `when` string[], required: Chip labels or free-text keywords, matched case-insensitively as substrings. A `when` that can match none of its step's `suggestions` is a validation error, not a step that never fires.
        - `ask` FlowStep, required (recursive)
    - `closing` string: Sent after the last answer. **Optional** — a flow whose purpose is to compute something and tell them has no useful closing to write in advance, and ends up sending filler followed by a second message carrying the actual answer. Omit it and the flow finishes silently: the `completed` webhook fires with the answers and your own reply is the last thing the recipient sees. If you omit it, the opt-out instruction must be in `disclaimer`, which is where carriers expect it anyway. An empty string is rejected — that would send a blank message.
    - `intro` string: Prepended to the first message, before step one.
    - `disclaimer` string: Appended to the **first outbound message only**: frequency, rates, HELP and STOP. This is where the opt-out instruction belongs — carriers expect it at the start of a programme.
    - `keyword` string: Inbound keyword that starts this flow. Recorded for reference; routing on it is yours.
    - `holdOpen` boolean: Leave the conversation OPEN after the last answer instead of closing it. A flow normally closes on completion — right for a survey, wrong when `completed` is where your work *starts*. With this set, the `completed` webhook fires, the flow stops asking, and the reply you compute is a normal turn rather than a message sent after the end. Close it yourself with `POST /v1/conversations/{id}/close` when you are done. **Goes inside `flow`, not beside it.** It was missing from this schema while the prose recommended it, so the first person to follow that advice put it at the top level, got a 200, and watched the conversation close anyway.
    - `card` FlowCard: Branding for the first message, rendered as an RCS standalone card. Sent as its own leading message, not wrapped around the question: a card description truncates after ~120 visible characters behind a chevron, so text inside a card is *less* visible than text outside it. SMS recipients never see it.
      - `title` string: Card heading, ≤200 characters. Defaults to the flow name.
      - `imageUrl` string, required: Publicly reachable image URL. Google fetches it directly and reads the MIME type from the response's `content-type`, so that header must be present and correct. **Redirects are not followed** — which rules out most CDN vanity URLs and every link shortener.
      - `thumbnailUrl` string: Optional, ≤100 kB. Without one the card shows a blank placeholder while loading.
      - `height` 'SHORT' | 'MEDIUM' | 'TALL': 112dp · 168dp · 264dp.

## Response `200`

Opened, and the first message sent.

- object
  - `conversationId` string, required
  - `providerMsgId` string: Opening provider message ID; absent when a flow-only opening is queued.
  - `channel` 'rcs' | 'sms' | 'queued', required
  - `channelConfidence` 'provisional' | 'committed': `provisional` means we chose a channel and have no confirmation yet. It becomes `committed` when the channel is proven: on the first delivery receipt from the handset on RCS, or on an SMS fallback **caused by the recipient** — no RCS on their device, or your agent not launched on their carrier. A fallback caused by *us* — a rejected payload, a bad credential, a timeout — leaves this `provisional`, because it says nothing about the recipient. Committing there would mark a perfectly reachable handset SMS-only for the rest of the conversation on the strength of our own bug, and report it as a fact about their phone. See `fellBack.ourFault` on the send response.
  - `trustLevel` 'verified_rcs' | 'branded_sms' | 'unbranded_sms'
  - `fellBack` FellBack
    - `cause` 'recipient' | 'sender', required: `recipient`: their handset has no RCS, or your agent is not launched on their carrier. `sender`: we were rejected — a bad payload, a media URL Google could not fetch, a timeout. Nothing was learned about the recipient.
    - `reason` string, required: Google's or the carrier's own words, trimmed.
    - `ourFault` boolean, required: True when the fallback says nothing about the recipient. Retrying rich is reasonable once the reason is fixed.
    - `remediation` string
    - `mediaRejected` object: Present when Google refused the message because of the file behind `media.url` or a card's media. The URL must answer GET and HEAD with 200, a real Content-Type and Content-Length, over public https with no login. The recipient is reachable; the SMS carried the fallback text.
      - `url` string, required: The file Google refused.
      - `stage` 'google_fetch', required: Google fetches `media.url` itself; we never see the HTTP exchange.
      - `status` number: The HTTP status Google's error names, when it names one.
      - `reason` string, required: Google's sentence about the file.
  - `serverDriven` boolean: Present when a flow is attached — Cadence answers replies for you.
  - `cardSent` boolean: Whether `flow.card` was rendered. RCS only.

## Other responses

- `400`: Validation failed, or `opening` is missing with no flow attached.
- `401`: Missing, unknown or revoked API key.
- `403`: Key suspended, or scoped to a different brand than the one addressed.
- `404`: Brand not found.
- `409`: Recipient opted out, or an idempotency conflict.
- `422`: The flow can't be run, or the opening isn't step one. **Nothing was sent and no conversation was opened.**
- `500`: Unexpected server error. The body is always JSON.
- `503`: This deployment can't send on a key of this kind.

---

[API](https://skmtc.dev/cadencercs/apis/cadence-api.md) · [All operations](https://skmtc.dev/cadencercs/apis/cadence-api/llms.txt) · [OpenAPI document](https://skmtc.dev/cadencercs/apis/cadence-api/revisions/9438443c2081?raw)
