Send your first reply
QuickstartGet 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
Section titled “Connect and reply”Get a test key
From your project folder:
npx @flow-engineer/messaging initThis 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.
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.
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.
npm install @flow-engineer/messaging# Save the TypeScript example below as agent.ts.node --env-file=.env agent.tspip install flow-messaging# Set FLOW_MESSAGING_KEY in the shell, then:python agent.pyimport { 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, });}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.
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 or choose your agent framework.
Sandbox limits
Section titled “Sandbox limits”An anonymous test key allows one contact and 50 outbound messages, and expires after seven days. Sign in to keep the app and use the signed-in allowance. Use synthetic data and shared sandbox senders only for testing.
Use plain HTTP
Section titled “Use plain HTTP”The API works from any language. These alternatives use the same key and conversation:
Receive and reply with 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
// 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 and sends nothing;
its hint says what to send instead.
Moving from the older API
Section titled “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/messaging0.4.0,flow-messaging0.2.0): have your coding agent run theflow-messaging-migrateskill. In Claude Code, runclaude plugin marketplace add flow-engineer/sdk, thenclaude plugin install flow-messaging@flow-engineer. The skill’s source is on GitHub. - Flow Framework: upgrade
@flow-engineer/flowto 0.7.0. Your agent code does not change.
Thanks, we read every comment.