> ## 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.

# Keys and sign-in

> Create a test key, check your allowance and claim the app with your Flow account.

Anyone, including an AI coding agent, can get a test key without an account. A person signs in later, with a Flow account (flow\.engineer: Google, GitHub or an email link), to keep the app and get more room in the sandbox.

## 1. Get a key in one call

First check whether you already have one: if `FLOW_MESSAGING_KEY` is set (in the environment or `.env`), use it and skip this step.

```bash curl theme={null}
curl -X POST https://api.flow.engineer/v0/sandbox/keys
# Optional name, shown in the dashboard once the app is claimed:
# -H "Content-Type: application/json" -d '{"name": "Support agent"}'
```

```bash CLI theme={null}
npx @flow-engineer/messaging init
```

```ts TypeScript theme={null}
import { FlowMessaging } from "@flow-engineer/messaging";

const flow = new FlowMessaging(); // no key needed for this call
const sandbox = await flow.sandbox.createKey({ name: "Support agent" });
// save sandbox.key as FLOW_MESSAGING_KEY and sandbox.claim_token as FLOW_CLAIM_TOKEN
```

> **Example key response**
>
> The answer (`201`, a `SandboxKey`, shortened here) is a new app of its own:
>
> ```json theme={null}
> {
>   "key": "fk_test_...",
>   "claim_token": "fct_...",
>   "claim_url": "https://api.flow.engineer/admin/claim#token=fct_...",
>   "api_key": { "id": "key_...", "mode": "test", "expires_at": "2026-10-17T09:30:00Z" },
>   "app": { "id": "app_...", "name": "Sandbox app", "sandbox_join_code": "wild-otter-04508705" },
>   "allowance": { "tier": "anonymous", "contacts": { "limit": 1, "used": 0 }, "messages": { "limit": 50, "used": 0, "remaining": 50 } },
>   "senders": [
>     { "id": "snd_...", "channel": "telegram", "kind": "shared", "address": { "link": "https://t.me/..." }, "join_code": "Hi Sandbox app (ref wild-otter-04508705)" }
>   ]
> }
> ```

* **`key` and `claim_token` are shown once.** Save `key` as `FLOW_MESSAGING_KEY` and `claim_token` as `FLOW_CLAIM_TOKEN`, in the environment or a git-ignored `.env`. The claim token (and `claim_url`, which holds it) lets a person claim the app, so treat it like the key.
* **Join from a phone:** open a sender's `address.link` (on Telegram, tap **Start**), or send it the `join_code` (the whole message, as it is).
* **Don't call it again** when you already have a key. Calls are limited per client address and network; going over answers `429 rate_limited` with `retry_after`.

`init` does all of this: with no key in the environment or `.env`, it gets one itself, writes `FLOW_MESSAGING_KEY` and `FLOW_CLAIM_TOKEN` to `.env`, and prints the sandbox link and join code, the allowance and the expiry. To use a key you already have, pass `init --key fk_test_...`.

## 2. The sandbox allowance

Test keys send only on Flow's shared sandbox senders, and only to people who joined your app there. What an app may send there for free:

| | No account (key from `POST /v0/sandbox/keys`) | Signed in (Flow account) |
| - | - | - |
| Contacts | 1 | 3 |
| Messages | 50 in total | 100 per contact |
| Channels | Telegram sandbox (WhatsApp when its sandbox opens) | Telegram sandbox (WhatsApp when its sandbox opens) |
| Key expiry | 7 days (`api_key.expires_at`) | none |

* Only messages your agent sends count. Inbound messages are free.
* A contact counts once it joins your app, and keeps counting after it leaves.
* iMessage is not part of either allowance. Flow's shared iMessage line comes with the account's free trial: your first sign-in at flow\.engineer (the dashboard), or the first key you make there, starts it, and the Flow CLI's `npx @flow-engineer/flow setup` starts the same trial. It has room for up to 25 people per channel, with test and live keys, and your own account's apps use it instead of the signed-in allowance (see [Pricing](https://docs.flow.engineer/unified-messaging-api/pricing.md)). Dedicated lines are arranged with the Flow team.
* The WhatsApp sandbox is not open yet; when it opens, the allowance covers it.
* When a key from `POST /v0/sandbox/keys` expires, its keys stop working and its contact is removed from the sandbox. A person can still sign in and claim the app.
* Going live: signed in, you make `fk_live_` keys in the dashboard ([Keys, Live](https://api.flow.engineer/admin/keys?mode=live)) and connect your own Telegram bot or WhatsApp number; dedicated iMessage lines are arranged with the Flow team. See [Going live](https://docs.flow.engineer/unified-messaging-api/guides/going-live.md).

See what is left with `GET /v0/app` (shortened):

```bash theme={null}
curl https://api.flow.engineer/v0/app -H "Authorization: Bearer $FLOW_MESSAGING_KEY"
```

```json theme={null}
{
  "allowance": {
    "tier": "anonymous",
    "channels": ["telegram", "whatsapp"],
    "contacts": { "limit": 1, "used": 1 },
    "messages_per_contact": 50,
    "messages": { "limit": 50, "used": 12, "remaining": 38 },
    "expires_at": "2026-10-17T09:30:00Z",
    "upgrade": "Sign in to keep this app and send 100 messages to each of 3 contacts: npx @flow-engineer/messaging login"
  }
}
```

In TypeScript: `(await flow.app.retrieve()).allowance`.

## 3. Sign in to keep the app

Sign in with your Flow account to claim the app and keep its data. The signed-in allowance is
shared across your apps: three contacts, 100 messages each, with up to ten claimed apps.

* **CLI or device flow:** the new key replaces the sandbox key, which stops working immediately.
* **Browser claim:** no new key is returned. The old key is revoked unless you select
  **Keep my agent's current key working**.
* **Kept keys:** expiry is removed. An expired key can work again if the app is claimed within
  30 days of expiry. For a revoked key, create a replacement in the dashboard.

### With the CLI

```bash theme={null}
npx @flow-engineer/messaging login
```

It reads `FLOW_CLAIM_TOKEN` from `.env`, prints a link and a short code (for example `WDJB-MJHT`), opens the browser and waits. Once the person approves, it replaces `FLOW_MESSAGING_KEY` in `.env` with the new key and removes `FLOW_CLAIM_TOKEN`. `--no-browser` skips opening the browser.

**Coding agents that cannot wait** on a command: run `npx @flow-engineer/messaging login --no-wait`. It prints the link and code and exits. Show them to the person, and once they say they approved, run `npx @flow-engineer/messaging login` again to collect the key.

### With curl

1. Start the sign-in with the claim token. `client_name` is shown to the person on the approval page.

   ```bash theme={null}
   curl -X POST https://api.flow.engineer/v0/device/authorizations \
     -H "Content-Type: application/json" \
     -d '{"claim_token": "'"$FLOW_CLAIM_TOKEN"'", "client_name": "Claude Code"}'
   ```

   ```json theme={null}
   {
     "device_code": "fdc_...",
     "user_code": "WDJB-MJHT",
     "verification_uri": "https://api.flow.engineer/admin/device",
     "verification_uri_complete": "https://api.flow.engineer/admin/device?code=WDJB-MJHT",
     "expires_in": 900,
     "interval": 5
   }
   ```

   Without a claim token, send the app's test key instead (`-H "Authorization: Bearer $FLOW_MESSAGING_KEY"`).

2. Show the person `verification_uri` and `user_code`: they open the page, sign in and type the code you show them (a link alone never approves; `verification_uri_complete` opens the same page). Never show `device_code`.

3. Poll every `interval` seconds:

   ```bash theme={null}
   curl -X POST https://api.flow.engineer/v0/device/token \
     -H "Content-Type: application/json" -d '{"device_code": "fdc_..."}'
   ```

   * `pending`: wait `interval` seconds and poll again. `429 rate_limited` means slow down: wait `retry_after` seconds.
   * `approved`: the answer holds `key` (shown once). Save it as `FLOW_MESSAGING_KEY` in place of the sandbox key, and drop `FLOW_CLAIM_TOKEN`.
   * `denied`: the person refused. Stop.
   * `expired`: the codes ran out (after 15 minutes) or were already used. Start again.

### With TypeScript

```ts theme={null}
import { DeviceSignInError, FlowMessaging } from "@flow-engineer/messaging";

const flow = new FlowMessaging();
try {
  const token = await flow.device.signIn({
    claimToken: process.env.FLOW_CLAIM_TOKEN,
    clientName: "My agent",
    prompt: (auth) => console.log(`Open ${auth.verification_uri_complete} and check the code ${auth.user_code}`),
  });
  // token.key replaces FLOW_MESSAGING_KEY
} catch (err) {
  if (err instanceof DeviceSignInError) console.log(`Sign-in ${err.reason}`); // "denied" or "expired"
  else throw err;
}
```

`signIn` polls for you, honouring `interval` and `retry_after`. For your own loop, use `flow.device.authorize({ claimToken, clientName })` and `flow.device.poll(deviceCode)`.

### In a browser

`claim_url` opens a page where the person signs in and claims the app without the CLI.

## The dashboard

> **Dashboard and account linking**
>
> Signed-in people manage their apps and keys at [api.flow.engineer/admin](https://api.flow.engineer/admin) (sign in with a Flow account), including creating and revoking test and live keys. A Flow account is the one you use on flow\.engineer: sign in there with Google, GitHub or an email link. [flow.engineer/login](https://flow.engineer/login) is the only sign-in page: the dashboard and the device flow send you there and bring you back, at once when you are already signed in on flow\.engineer. If you used the dashboard with GitHub before Flow accounts, sign in once on flow\.engineer with the same email and ask the Flow team to link the two, so you keep your apps and keys; accounts are never linked by email on their own.

## Errors

> **Allowance and sign-in errors**
>
> | Error | `channel_code` | What it means | What to do |
> | - | - | - | - |
> | `403 permission` | `sandbox_allowance_used` | The app sent all the messages its allowance allows. | Sign in (`login`) for 100 per contact for 3 contacts, shared by all your apps. Signed in already: go live. |
> | `403 permission` | `sandbox_contact_limit` | The allowance has no room for another contact. | Sign in for 3 contacts, or keep testing with the contact who joined. |
> | `403 permission` | `sandbox_channel_not_included` | The send used a sandbox channel the allowance does not cover, such as iMessage. | Use the Telegram sandbox sender. For the shared iMessage line, sign in at flow\.engineer to start the free trial (or run `npx @flow-engineer/flow setup`). |
> | `403 permission` | `sign_in_required` | An app made without an account tried something that needs a person signed in, such as connecting its own Telegram bot. | Sign in (`login`), then retry. |
> | `401 authentication` | `sandbox_key_expired` | The key from `POST /v0/sandbox/keys` is past its `expires_at` (7 days). | Sign in to claim the app within 30 days of the expiry (the key works again, and you get a new one), or get a new key with `POST /v0/sandbox/keys`. |
>
> Switch on `error.type` and read `error.channel_code`; each error also carries a `hint` and a `doc_url` ([permission](https://docs.flow.engineer/unified-messaging-api/errors/permission.md), [authentication](https://docs.flow.engineer/unified-messaging-api/errors/authentication.md), [rate\_limited](https://docs.flow.engineer/unified-messaging-api/errors/rate_limited.md)).


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