> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow.engineer/llms.txt
> Use this file to discover all available pages before exploring further.

# Test and live mode

> Keep sandbox data and real customer conversations separate.

Every API key belongs to one app and one **mode**, and the mode decides who your agent can reach.

```bash theme={null}
curl https://api.flow.engineer/v0/app -H "Authorization: Bearer $FLOW_MESSAGING_KEY"
# "livemode": false  -> a test key
```

| | Test mode | Live mode |
| - | - | - |
| Key | `fk_test_...` | `fk_live_...` |
| Senders | The shared sandbox senders | Your dedicated senders |
| Who can be messaged | Only people who sent your app's join code | Anyone the channel's rules allow |
| Data | Test conversations, events, files and webhook endpoints | Live ones |

## Rules

* **Nothing in test mode reaches a real contact** unless that person joined the sandbox through your app. Build and test freely.
* **The modes never mix.** Every object has `livemode`. A test key cannot see live data or use a live sender (`403` [`permission`](https://docs.flow.engineer/unified-messaging-api/errors/permission)), and webhook endpoints receive only their own mode's events.
* **A key is shown once**, when it is issued, and stored only as a hash. Get a test key with no account in one call (`curl -X POST https://api.flow.engineer/v0/sandbox/keys`, or `npx @flow-engineer/messaging init`); signed in, you make live keys (`fk_live_...`) in the dashboard for your own Telegram bot or WhatsApp number (iMessage lines are arranged with the Flow team). Signed-in people create and revoke keys in the [dashboard](https://api.flow.engineer/admin); otherwise ask the Flow team to revoke a key if it leaks.
* **Test mode has a sandbox allowance.** Without an account: 1 contact, 50 messages in total, and the key expires after 7 days (`api_key.expires_at`). Signed in with a Flow account: 3 contacts, 100 messages each, no expiry, one allowance per person shared by all their apps. It covers the Telegram sandbox (and WhatsApp when its sandbox opens), not iMessage, and counts only messages your agent sends. Your own account's apps get the free trial instead, from your first sign-in at flow\.engineer: up to 25 people per channel on Flow's shared senders, the shared iMessage line included, with messages not capped (see [Pricing](https://docs.flow.engineer/unified-messaging-api/pricing.md)); an app claimed from a sandbox key keeps the allowance. `GET /v0/app` returns what is left (`allowance`). See [Keys and sign-in](https://docs.flow.engineer/unified-messaging-api/get-a-key.md).
* **Keep live keys on your server.** Never ship a key in a web page or a mobile app.

## Simulated contacts: test without a phone

A test key can also play the person's side, so an automated test suite (or an agent
checking its own work) runs end to end with no phone and no real channel. A
**simulated contact** exists only inside Flow, on Telegram, WhatsApp, iMessage or SMS.

```bash theme={null}
# A conversation with a new simulated contact (made-up address; one per test case)
curl -s https://api.flow.engineer/v0/test/conversations -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
  -H "Content-Type: application/json" -d '{"channel": "sms"}'
# Write as the contact (media: upload with POST /v0/files, then pass file_id)
curl -s https://api.flow.engineer/v0/test/conversations/conv_.../inbound -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
  -H "Content-Type: application/json" -d '{"content": {"kind": "basic", "text": "Do you deliver on Sundays?"}}'
# Both sides, oldest first: what was sent is exactly what the contact sees
curl -s https://api.flow.engineer/v0/test/conversations/conv_.../transcript -H "Authorization: Bearer $FLOW_MESSAGING_KEY"
```

* Each turn reaches your agent exactly like a real message: `message.received` by
  webhook, on the stream and in the event log, in order, with `simulated: true` on
  the conversation.
* Your agent replies with its normal sends. They pass the real send gate (the
  channel's kinds and fields, text limits, edit targets) and end `message.sent` on
  your app's simulated sender (`kind: simulated`), which never touches a network.
  A kind the channel does not take answers `422`
  [`unsupported_content`](https://docs.flow.engineer/unified-messaging-api/errors/unsupported_content), as on a real sender.
* A turn is a kind object in `content`: `basic` (`text` and/or `media` by
  `file_id`), `voice`, `location`, `contact_card`, or `button_reply` (a tap:
  `message_id` of your buttons, carousel or list message and the tapped
  `button_id`; Flow fills in the label).
* **Free:** simulated conversations take no sandbox contact slot and use no
  messages, since nothing leaves Flow. That holds on an app made without an account
  too: its one contact stays free for your own phone, so test on the app you have
  rather than making a second one. Simulated conversations, contacts and the events'
  `conversation` carry `simulated: true`; real ones have no `simulated` field. The key's request rate limit still applies, and the gate skips
  the rules that protect a real number (pacing, new-contact budgets, abuse
  throttling), so tests can run in parallel.
* **SMS rules hold for the contact.** Before a simulated SMS contact's first turn,
  a send to them is a start that needs `consent` on `POST /v0/messages`, as on a
  real number (`403` [`permission`](https://docs.flow.engineer/unified-messaging-api/errors/permission), `consent_required`). A
  turn with the text STOP opts the contact out (`opted_out_via` `keyword`; sends
  answer `contact_opted_out`) and START lifts it; the transcript shows no
  automatic keyword reply. A simulated SMS sender keeps no quiet hours, so a test
  never depends on the time of day, and it sends media as MMS.
* **WhatsApp rules hold too.** The contact's turns go through the real WhatsApp
  inbound path, so each turn opens the 24-hour window; outside it (also before the
  contact's first turn) only a `template` can be sent (`409`
  [`outside_window`](https://docs.flow.engineer/unified-messaging-api/errors/outside_window)). Buttons take up to 3 reply buttons
  (a `list` up to 10 rows); reactions and `reply_to` work; edits and unsends are
  refused (`422` [`unsupported_content`](https://docs.flow.engineer/unified-messaging-api/errors/unsupported_content)).
* **iMessage group chats can be simulated.** `POST /v0/test/conversations` with
  `{"channel": "imessage", "group": {"name": "Weekend trip", "participants": [{"name": "Asha"}, {"name": "Ben"}]}}`
  makes a `group` conversation (2 to 32 participants, each a simulated contact).
  Each turn names its writer: `from` (a participant's `contact`) is required on
  `/inbound` there. The group rules hold: reactions, typing and read receipts are
  refused (`422` [`unsupported_content`](https://docs.flow.engineer/unified-messaging-api/errors/unsupported_content),
  `group_unsupported`).
* **HIPAA accounts** simulate only SMS: a simulated Telegram, WhatsApp or iMessage
  conversation, or a turn in one made before the flag was set, answers `403`
  [`permission`](https://docs.flow.engineer/unified-messaging-api/errors/permission) (`hipaa_channel_not_covered`).
* Live keys get `403` [`permission`](https://docs.flow.engineer/unified-messaging-api/errors/permission) (`test_mode_only`).
  `GET /v0/senders` lists simulated senders only with `kind=simulated`.

In TypeScript: `flow.test.createConversation({ channel: "sms" })`,
`flow.test.inbound(convId, "hi")` and `flow.test.transcript(convId)`.

## Going from test to live

Your code does not change. Connect your own Telegram bot or WhatsApp number (or have the Flow team connect an iMessage line), switch `FLOW_MESSAGING_KEY` to a live key, and register your webhook endpoint again with the live key. See [Going live](https://docs.flow.engineer/unified-messaging-api/guides/going-live.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.