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

> ## Agent Instructions
> Flow Messaging is one HTTP API (https://api.flow.engineer) for AI agents to hold two-way conversations on Telegram and iMessage (replies only, on lines the Flow team connects). WhatsApp is coming and not available yet.
> Build the app on the REST API. MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript SDK, which wraps it) for everything it does at runtime. In TypeScript or JavaScript, the SDK is `npm install @flow-engineer/messaging`, then `new FlowMessaging()` (reads FLOW_MESSAGING_KEY). Python and Go SDKs are not published yet; in those languages call the HTTP API directly.
> No key yet? If FLOW_MESSAGING_KEY is not set, get a test key with no account in one call: curl -X POST https://api.flow.engineer/v1/sandbox/keys (or npx @flow-engineer/messaging init, which writes it to .env). Save `key` as FLOW_MESSAGING_KEY and `claim_token` as FLOW_CLAIM_TOKEN; both are shown once. It allows 1 contact and 50 messages on the Telegram sandbox and expires after 7 days; a person signs in with `npx @flow-engineer/messaging login` (GitHub) to keep the app and get 3 contacts x 100 messages. Signed in, live keys (fk_live_...) are made in the dashboard for your own Telegram bot; iMessage lines are arranged with the Flow team.
> The hosted MCP server (https://api.flow.engineer/mcp) is optional, for testing during development. Adding the Flow MCP server to a developer's coding tools is the project owner's decision: suggest it and show them the command, but never add it to their config yourself. The command for Claude Code: claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY". If its tools are already available to you, use them for sandbox testing.
> Authenticate with `Authorization: Bearer fk_test_...` (sandbox) or `fk_live_...` (real contacts). Never put a live key in client-side code.
> Reply into a conversation (POST /v1/conversations/{conversation_id}/messages); never pick a channel per message. Switch on `error.type`, not the message text.
> Full documentation for agents: https://docs.flow.engineer/llms-full.txt. OpenAPI spec: https://raw.githubusercontent.com/flow-engineer/sdk/main/openapi/openapi.yaml.

# Quickstart: give your AI agent Telegram in 5 minutes

> Get a test key, join the Flow Telegram sandbox from your phone, receive your first message and reply to it, in TypeScript, or over HTTP from Python, Go or curl. Five minutes, no bot of your own needed.

This page gets an agent answering messages on your phone in about 5 minutes: get a test key, join the shared Telegram sandbox, receive a message, and reply. The same code later answers on your own Telegram bot, and on an iMessage line the Flow team connects for you (WhatsApp is coming).

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

That one command gets a test key with no account, saves it to `.env` as `FLOW_MESSAGING_KEY` (with `FLOW_CLAIM_TOKEN`), offers to set up your coding agent (skill, `AGENTS.md`, MCP server; it asks you first), and prints the sandbox link and join code. It asks before setting up your coding agent; `--yes` skips the question. The steps below do the same by hand and then build the agent.

<Steps>
  <Step title="Get a test key">
    Already have one in `FLOW_MESSAGING_KEY`? Skip this step. Otherwise get one in one call, with no account:

    ```bash theme={null}
    curl -X POST https://api.flow.engineer/v1/sandbox/keys
    ```

    The answer holds `key` (`fk_test_...`) and `claim_token` (`fct_...`), both shown once, plus your `app` (with its `sandbox_join_code`) and the sandbox `senders`. Save them:

    ```bash theme={null}
    export FLOW_MESSAGING_KEY=fk_test_...
    export FLOW_CLAIM_TOKEN=fct_...
    ```

    Test keys reach only the shared sandbox, so nothing reaches anyone who has not joined it. This key allows 1 contact and 50 messages on the Telegram sandbox and expires after 7 days; [sign in](/get-a-key#3-sign-in-to-keep-the-app) with GitHub (`npx @flow-engineer/messaging login`) to keep the app and send 100 messages to each of 3 contacts. See [Keys and sign-in](/get-a-key).

    Check the key, and read your app's sandbox join code and what is left of its allowance:

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

    The answer includes `app.sandbox_join_code`, for example `wild-otter-04508705`, and `allowance`.
  </Step>

  <Step title="Join the sandbox from your phone">
    List the sandbox senders your key can use (today, the Telegram sandbox bot):

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

    Each shared sender has an `address.link` and a `join_code`. Open the link on your phone:

    * **Telegram:** the link is `https://t.me/<bot>?start=<code>`. Open it and tap **Start**. That's it, you've joined.
    * **If the link can't be used** (for example you found the bot by searching for it): send the sender's `join_code` to it, for example:

    ```text theme={null}
    join wild-otter-04508705
    ```

    Your app now has a conversation with you, and you receive a `conversation.started` event. The join code is how a shared sandbox sender knows which app a person belongs to, so no stranger is ever messaged. See [Senders](/concepts/senders).
  </Step>

  <Step title="Receive a message and reply">
    Run one of these, then send any message to the sandbox sender from your phone. Each program waits for `message.received` events and answers into the same conversation.

    <CodeGroup>
      ```ts TypeScript theme={null}
      // npm install @flow-engineer/messaging
      import { FlowMessaging, contentText } from "@flow-engineer/messaging";

      const flow = new FlowMessaging(); // reads FLOW_MESSAGING_KEY

      for await (const event of flow.events.stream({ types: ["message.received"] })) {
        const said = contentText(event.data.message.content);
        console.log(`${event.conversation.channel}: ${said}`);
        await event.conversation.reply(`You said: ${said}`);
      }
      ```

      ```python Python (HTTP) theme={null}
      # The Python SDK is not published yet; this is the same agent over plain HTTP.
      # pip install requests
      import os, time, requests

      API = "https://api.flow.engineer"
      HEADERS = {
          "Authorization": f"Bearer {os.environ['FLOW_MESSAGING_KEY']}",
          "Flow-Version": "2026-11-01",
      }

      after = None
      while True:
          params = {"type": "message.received", "limit": 100}
          if after:
              params["after"] = after
          page = requests.get(f"{API}/v1/events", headers=HEADERS, params=params, timeout=30).json()
          for event in page["data"]:
              after = event["id"]
              content = event["data"]["message"]["content"]
              said = content.get("text") or content.get("caption") or content.get("transcript") or ""
              requests.post(
                  f"{API}/v1/conversations/{event['conversation']['id']}/messages",
                  headers={**HEADERS, "Idempotency-Key": event["id"]},
                  json={"content": {"type": "text", "text": f"You said: {said}"}},
                  timeout=30,
              )
          if not page["has_more"]:
              time.sleep(1)
      ```

      ```go Go (HTTP) theme={null}
      // 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 {
      				Type string `json:"type"`
      				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", "/v1/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{
      				"content": map[string]any{"type": "text", "text": "You said: " + ev.Data.Message.Content.Text},
      			}
      			if err := call("POST", "/v1/conversations/"+ev.Conversation.ID+"/messages", reply, ev.ID, nil); err != nil {
      				fmt.Println(err)
      			}
      		}
      		if !page.HasMore {
      			time.Sleep(time.Second)
      		}
      	}
      }
      ```

      ```bash curl theme={null}
      # 1. Read the newest messages from the event log (oldest first; pass after=evt_... to page forward).
      curl -G https://api.flow.engineer/v1/events \
        -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
        --data-urlencode "type=message.received"

      # 2. Reply into the conversation from an event (data[].conversation.id).
      curl https://api.flow.engineer/v1/conversations/conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1/messages \
        -H "Authorization: Bearer $FLOW_MESSAGING_KEY" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: $(uuidgen)" \
        -d '{"content": {"type": "text", "text": "Hello from my agent"}}'
      ```
    </CodeGroup>

    The send answers `202` with the queued message. Its progress may arrive as `message.sent`, `message.delivered` and `message.read` events, depending on what the channel reports. To know how a send ended, wait for `message.sent` or `message.failed`: every channel reports one of them.
  </Step>

  <Step title="Answer with your model">
    Replace the echo with your agent. In TypeScript, pass the model's stream straight to `reply()`: it keeps the typing indicator on and sends the answer as chat bubbles while the model writes.

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

    const flow = new FlowMessaging();
    const openai = new OpenAI();

    for await (const event of flow.events.stream({ types: ["message.received"] })) {
      await event.conversation.reply(
        openai.chat.completions.create({
          model: "gpt-4.1-mini",
          stream: true,
          messages: [{ role: "user", content: contentText(event.data.message.content) }],
        }),
      );
    }
    ```

    Without the SDK, split the answer at paragraph breaks and send each part as its own message, as [Streaming replies](/concepts/streaming-replies) describes.
  </Step>
</Steps>

## What you built

* A test-mode agent that answers anyone who joined your app on the Telegram sandbox. The code does not change per channel: it answers the same way on your own Telegram bot or an iMessage line.
* It reads events from the live stream (TypeScript) or the event log (HTTP). In production most agents use [webhooks](/concepts/events-and-webhooks) instead, and can answer straight in the webhook response.

## Next steps

<CardGroup cols={2}>
  <Card title="Events and webhooks" icon="webhook" href="/concepts/events-and-webhooks">
    Signed deliveries, retries, ordering, and replying in the response.
  </Card>

  <Card title="Local development" icon="laptop-code" href="/guides/local-development">
    Receive events on your laptop with no public URL through the live stream.
  </Card>

  <Card title="Content types" icon="shapes" href="/concepts/content-types">
    Media, voice notes, buttons, reactions and what each channel shows.
  </Card>

  <Card title="Going live" icon="flag-checkered" href="/guides/going-live">
    A live key, your own Telegram bot, and iMessage lines.
  </Card>
</CardGroup>


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