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

# Send your first reply

> Get a test key, join the Telegram sandbox and answer a message.

Get your agent answering on your phone. Start with the Telegram sandbox; the same handler later
works with your own bot, WhatsApp number or a Flow-connected iMessage line.

## Connect and reply

**1. Get a test key**

From your project folder:

```sh Terminal
npx @flow-engineer/messaging init
```

This creates a test key without an account, writes `FLOW_MESSAGING_KEY` and
`FLOW_CLAIM_TOKEN` to `.env`, and prints a sandbox link. It asks before installing coding-agent
support or configuring the optional MCP connection.

Already have a key? Use it instead of creating another app.

**2. Join from your phone**

Open the printed Telegram link and tap **Start**. If you found the bot another way, send
the join message printed by the CLI.

**Check it worked:** your app has a conversation with your phone. The sandbox only sends
to people who have joined it.

**3. Run the reply handler**

Choose an SDK, install it and set `FLOW_MESSAGING_KEY` in your process environment.
Node does not automatically load `.env`; use the run command below.

```sh TypeScript
npm install @flow-engineer/messaging
# Save the TypeScript example below as agent.ts.
node --env-file=.env agent.ts
```

```sh Python
pip install flow-messaging
# Set FLOW_MESSAGING_KEY in the shell, then:
python agent.py
```

```ts agent.ts
import { FlowMessaging, contentText } from "@flow-engineer/messaging";

const flow = new FlowMessaging();
for await (const event of flow.events.stream({ types: ["message.received"] })) {
  const said = contentText(event.data.message.content);
  await event.conversation.reply("You said: " + said, {
    idempotencyKey: event.id,
  });
}
```

```python agent.py
from flow_messaging import Flow, content_text

flow = Flow()
for event in flow.events.stream(types=["message.received"]):
    said = content_text(event.data.message.content)
    flow.messages.send(
        event.conversation.id,
        f"You said: {said}",
        idempotency_key=event.id,
    )
```

**Check it worked:** text “hello” to the bot and receive “You said: hello”.
A send is queued first; watch `message.sent` or `message.failed` to learn its outcome.

**4. Use your agent's answer**

Replace the echo with a call to your model or agent. The TypeScript SDK's `reply()` also
accepts supported model streams and sends complete chat bubbles as they become ready.

See [streaming replies](https://flow.engineer/docs/unified-messaging-api/concepts/streaming-replies.md) or choose your
[agent framework](https://flow.engineer/docs/unified-messaging-api/frameworks/vercel-ai-sdk.md).

## Sandbox limits

An anonymous test key allows one contact and 50 outbound messages, and expires after seven days.
[Sign in](https://flow.engineer/docs/unified-messaging-api/get-a-key.md#3-sign-in-to-keep-the-app) to keep the app and use the signed-in allowance.
Use synthetic data and shared sandbox senders only for testing.

## Use plain HTTP

The API works from any language. These alternatives use the same key and conversation:

> **Receive and reply with curl**
>
> ```bash curl
> # 1. Read the newest messages from the event log (oldest first; pass after=evt_... to page forward).
> curl -G https://api.flow.engineer/v0/events \
>   -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
>   --data-urlencode "type=message.received"
>
> # 2. Reply into the conversation from an event (data[].conversation.id) with a basic message.
> curl https://api.flow.engineer/v0/conversations/conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1/basic \
>   -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
>   -H "Content-Type: application/json" \
>   -H "Idempotency-Key: $(uuidgen)" \
>   -d '{"text": "Hello from my agent"}'
> ```

> **Run the Go HTTP example**
>
> ```go Go (HTTP)
> // The Go SDK is not published yet; this is the same agent over plain HTTP,
> // standard library only.
> package main
>
> import (
> 	"bytes"
> 	"encoding/json"
> 	"fmt"
> 	"net/http"
> 	"net/url"
> 	"os"
> 	"time"
> )
>
> const api = "https://api.flow.engineer"
>
> type event struct {
> 	ID           string `json:"id"`
> 	Conversation struct {
> 		ID string `json:"id"`
> 	} `json:"conversation"`
> 	Data struct {
> 		Message struct {
> 			Content struct {
> 				Kind string `json:"kind"` // basic, voice, button_reply, ...
> 				Text string `json:"text"`
> 			} `json:"content"`
> 		} `json:"message"`
> 	} `json:"data"`
> }
>
> func call(method, path string, body any, idempotencyKey string, out any) error {
> 	var buf bytes.Buffer
> 	if body != nil {
> 		if err := json.NewEncoder(&buf).Encode(body); err != nil {
> 			return err
> 		}
> 	}
> 	req, err := http.NewRequest(method, api+path, &buf)
> 	if err != nil {
> 		return err
> 	}
> 	req.Header.Set("Authorization", "Bearer "+os.Getenv("FLOW_MESSAGING_KEY"))
> 	req.Header.Set("Flow-Version", "2026-11-01")
> 	req.Header.Set("Content-Type", "application/json")
> 	if idempotencyKey != "" {
> 		req.Header.Set("Idempotency-Key", idempotencyKey)
> 	}
> 	res, err := http.DefaultClient.Do(req)
> 	if err != nil {
> 		return err
> 	}
> 	defer res.Body.Close()
> 	if res.StatusCode >= 300 {
> 		return fmt.Errorf("%s %s: %s", method, path, res.Status)
> 	}
> 	if out != nil {
> 		return json.NewDecoder(res.Body).Decode(out)
> 	}
> 	return nil
> }
>
> func main() {
> 	after := ""
> 	for {
> 		q := url.Values{"type": {"message.received"}, "limit": {"100"}}
> 		if after != "" {
> 			q.Set("after", after)
> 		}
> 		var page struct {
> 			Data    []event `json:"data"`
> 			HasMore bool    `json:"has_more"`
> 		}
> 		if err := call("GET", "/v0/events?"+q.Encode(), nil, "", &page); err != nil {
> 			fmt.Println(err)
> 			time.Sleep(2 * time.Second)
> 			continue
> 		}
> 		for _, ev := range page.Data {
> 			after = ev.ID
> 			reply := map[string]any{"text": "You said: " + ev.Data.Message.Content.Text}
> 			if err := call("POST", "/v0/conversations/"+ev.Conversation.ID+"/basic", reply, ev.ID, nil); err != nil {
> 				fmt.Println(err)
> 			}
> 		}
> 		if !page.HasMore {
> 			time.Sleep(time.Second)
> 		}
> 	}
> }
> ```

Each kind of message has its own endpoint (`/basic`, `/buttons`, `/list`, `/carousel`, ...), and its
page opens with the channels it works on. A kind, field or value the channel does not take answers
`422` [`unsupported_content`](https://flow.engineer/docs/unified-messaging-api/errors/unsupported_content) and sends nothing;
its `hint` says what to send instead.

## Moving from the older API

The API is V0: every path starts with `/v0`, and each kind of message has its own endpoint instead
of `POST /v1/conversations/{conversation_id}/messages` with `content`. There is no `fallback`,
`delivered_as`, markdown `format` or capabilities endpoint, typing takes no `state`, and starts are
flat (`{"sender": "snd_...", "to": {...}, "kind": "basic", "text": "Hi"}`).

* **Plain HTTP or the SDKs** (`@flow-engineer/messaging` 0.4.0, `flow-messaging` 0.2.0): have your
  coding agent run the `flow-messaging-migrate` skill. In Claude Code, run
  `claude plugin marketplace add flow-engineer/sdk`, then
  `claude plugin install flow-messaging@flow-engineer`. The skill's source is
  [on GitHub](https://github.com/flow-engineer/sdk/tree/main/plugin/skills/flow-messaging-migrate).
* **Flow Framework:** upgrade `@flow-engineer/flow` to 0.7.0. Your agent code does not change.

> **[Receive signed webhooks](https://flow.engineer/docs/unified-messaging-api/concepts/events-and-webhooks.md)**
>
> Use a public endpoint or forward events to localhost.

> **[Go live](https://flow.engineer/docs/unified-messaging-api/guides/going-live.md)**
>
> Connect your own sender and switch to a live key.
