---
title: "Deliver a reply as though the recipient sent it"
method: POST
path: "/v1/conversations/{id}/simulate-inbound"
tags: ["Flows"]
---

# Deliver a reply as though the recipient sent it

`POST /v1/conversations/{id}/simulate-inbound`

## Request

- Base URL: `https://api.cadencercs.com`
- URL: `https://api.cadencercs.com/v1/conversations/{id}/simulate-inbound`
- Auth: HTTP bearer

**Sandbox keys only.** This is how you exercise a flow — every branch, follow-up, closing and opt-out — without creating an agent at Google (irreversible) and waiting weeks for a carrier launch.

It runs the real inbound path rather than a lookalike, so what you see in sandbox is what the webhook will do. Refused on live keys: fabricating a message *from* a real person writes words they never sent into their transcript and consent record.

## Path parameters

- `id` string, required

## Request body

- object
  - `text` string: Something the recipient typed, e.g. `"182.4"`, `"STOP"` or `"START"`.
  - `optionIndex` number: A chip they tapped, **1-based** against the reply chips currently on offer. Delivered as a real `postback`, so handler code branching on `intent` can be exercised before live traffic. Out of range is refused, with the offered chips in the body. Action chips have no index — use `postbackData` for those.
  - `postbackData` string: A chip from **any recent message**, named by the slug we generated when we sent it. This is how you rehearse a *late tap*: someone reads the morning message and answers after the next one has already gone out. `optionIndex` cannot express that — it means "a chip on the newest message" — and a late tap behaves differently enough to be worth a test of its own. An unknown slug is refused with the ones we do know.

## Response `200`

What actually happened, so you can assert on it rather than poll.

- object
  - `ok` boolean, required
  - `delivered` string, required: The text that was delivered — for a chip tap, its label.
  - `as` 'text' | 'postback'
  - `optionIndex` number: Echoed when a chip was tapped.
  - `state` string, required: The conversation's state afterwards.
  - `lastOutbound` string, nullable: What Cadence sent back, **as the recipient would have received it** on their channel — the numbered fallback on the SMS leg.
  - `lastOutboundComposed` string, nullable: The same message as composed, before fallback rendering.
  - `flow` FlowAnswers: Structured results of a server-driven flow — the reason to run one. `outcome` and `final` exist so one request answers "is anything more coming". Without them `done:false, completion:0` meant three different things — still waiting, closed early, and opted out — and telling them apart cost a second call per poll.
    - `conversationId` string
    - `state` string: The conversation's own state, e.g. `OPEN`, `CLOSED`, `CLOSED_OPTOUT`.
    - `outcome` 'awaiting_reply' | 'completed' | 'closed_early' | 'opted_out'
    - `final` boolean: True when nothing further will arrive on this conversation.
    - `flowId` string, required
    - `flowName` string
    - `done` boolean, required: True once the closing has been sent.
    - `completion` number, required: Fraction of the flow's base steps answered, 0–1. Follow-ups are bonus and do not inflate it.
    - `currentStepId` string, nullable: Null when the flow is done.
    - `answers` object[], required
      - `stepId` string, required
      - `question` string, nullable: The step's text, so results read without holding the flow definition alongside.
      - `text` string, required: What they actually sent.
      - `optionIndex` number: **1-based position among the REPLY chips** — the same number the SMS fallback tells the recipient to reply with, so it means the same thing on both legs. Action chips are not counted and never carry one: there is no number you could reply with to open a URL. Absent on an action tap, which arrives with `optionLabel` and `chipAction` instead. `optionLabel` is provided so you never have to index at all.
      - `optionLabel` string: The matched chip's label.
      - `freeText` boolean: True when they typed something rather than choosing an offered option.

## Other responses

- `400`: `text` is required.
- `401`: Missing, unknown or revoked API key.
- `403`: Key suspended, or scoped to a different brand than the one addressed.
- `404`: No such conversation on this key or account.
- `409`: The conversation is CLOSED. Deliberately asymmetric with `POST /messages`, which still sends into one: a real inbound would not be routed to a closed conversation, so simulating it would tell you something untrue. An **opted-out** conversation (`CLOSED_OPTOUT`) is still simulatable, because production does route a START to one — otherwise the resubscribe path carriers require could not be rehearsed at all.
- `500`: Unexpected server error. The body is always JSON.

---

[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)
