curl --request POST \
--url https://api.flow.engineer/v1/senders \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({})
};
fetch('https://api.flow.engineer/v1/senders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/senders"
payload = {}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v1/senders"
payload := strings.NewReader("{}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"id": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel": "telegram",
"kind": "shared",
"livemode": true,
"status": "pending",
"address": {
"phone": "<string>",
"username": "<string>",
"handle": "<string>",
"link": "<string>"
},
"limits": {
"new_contacts_per_day": 123,
"new_contacts_per_hour": 123,
"whatsapp_tier": "<string>"
},
"created_at": "2023-11-07T05:31:56Z",
"display_name": "<string>",
"throttled_until": "2023-11-07T05:31:56Z",
"quality_rating": "green",
"join_code": "join brave-otter-40718263"
}{
"id": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel": "telegram",
"kind": "shared",
"livemode": true,
"status": "pending",
"address": {
"phone": "<string>",
"username": "<string>",
"handle": "<string>",
"link": "<string>"
},
"limits": {
"new_contacts_per_day": 123,
"new_contacts_per_hour": 123,
"whatsapp_tier": "<string>"
},
"created_at": "2023-11-07T05:31:56Z",
"display_name": "<string>",
"throttled_until": "2023-11-07T05:31:56Z",
"quality_rating": "green",
"join_code": "join brave-otter-40718263"
}{
"error": {
"type": "invalid_request",
"message": "limit must be between 1 and 100.",
"hint": "Pass limit between 1 and 100 (default 20), and page with after or before.",
"doc_url": "https://api.flow.engineer/docs/errors/invalid_request",
"param": "limit"
}
}{
"error": {
"type": "authentication",
"message": "No valid API key was given.",
"hint": "Send the header Authorization: Bearer fk_test_... (or fk_live_...); no key yet? Get a test key with curl -X POST https://api.flow.engineer/v1/sandbox/keys",
"doc_url": "https://api.flow.engineer/docs/errors/authentication"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}{
"error": {
"type": "new_contact_limit",
"message": "This sender has started its 15 new conversations for today.",
"hint": "Retry after 3600 seconds; replies into existing conversations still go.",
"doc_url": "https://api.flow.engineer/docs/errors/new_contact_limit",
"retry_after": 3600,
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}Request a dedicated sender
Connects a dedicated sender to your app. Today this connects a Telegram bot
(below). iMessage lines are connected by the Flow team, not by API: a request
with channel: "imessage" answers 501 not_implemented; ask the Flow team,
and the line then appears in GET /v1/senders. WhatsApp is not available
yet (it waits on Meta’s approval), so channel: "whatsapp" answers 501 not_implemented; once it ships, a WhatsApp number starts as pending, you
receive sender.status_changed when it is ready, and your business is
verified through Meta’s Embedded Signup. Live keys only.
To get a live key (fk_live_...), a person signs in to the dashboard at
https://api.flow.engineer/admin (GitHub), switches to Live,
and clicks Create live key on the Keys page
(https://api.flow.engineer/admin/keys?mode=live). An app made without an
account (POST /v1/sandbox/keys) is claimed first, by signing in through the
device flow or its claim_url. A test key gets 403 permission here, with
a hint naming these steps. Telegram bots are self-serve; iMessage lines are
arranged with the Flow team.
A Telegram bot is connected at once: give the token BotFather issued as
telegram_bot_token. Flow checks it, keeps it encrypted, points the bot’s
webhook at Flow, and answers 200 with the sender active. The token is
never returned. One bot is one sender:
- Connecting a bot that is already a sender of this app updates its token in
place and answers with the same sender (use it after revoking a token in
@BotFather; a sender
flaggedbecause Telegram rejected its old token becomesactiveagain, orthrottledwhile an abuse throttle still runs, and its queued messages go out). - Connecting a bot that is a sender of another app moves it here: holding
the token proves control of the bot. The old sender is retired (
banned) and its app receivessender.status_changed. - A bot that is one of Flow’s sandbox senders is refused with
403 permission.
To disconnect a bot, call DELETE /v1/senders/{sender_id}.
curl --request POST \
--url https://api.flow.engineer/v1/senders \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({})
};
fetch('https://api.flow.engineer/v1/senders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/senders"
payload = {}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v1/senders"
payload := strings.NewReader("{}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"id": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel": "telegram",
"kind": "shared",
"livemode": true,
"status": "pending",
"address": {
"phone": "<string>",
"username": "<string>",
"handle": "<string>",
"link": "<string>"
},
"limits": {
"new_contacts_per_day": 123,
"new_contacts_per_hour": 123,
"whatsapp_tier": "<string>"
},
"created_at": "2023-11-07T05:31:56Z",
"display_name": "<string>",
"throttled_until": "2023-11-07T05:31:56Z",
"quality_rating": "green",
"join_code": "join brave-otter-40718263"
}{
"id": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel": "telegram",
"kind": "shared",
"livemode": true,
"status": "pending",
"address": {
"phone": "<string>",
"username": "<string>",
"handle": "<string>",
"link": "<string>"
},
"limits": {
"new_contacts_per_day": 123,
"new_contacts_per_hour": 123,
"whatsapp_tier": "<string>"
},
"created_at": "2023-11-07T05:31:56Z",
"display_name": "<string>",
"throttled_until": "2023-11-07T05:31:56Z",
"quality_rating": "green",
"join_code": "join brave-otter-40718263"
}{
"error": {
"type": "invalid_request",
"message": "limit must be between 1 and 100.",
"hint": "Pass limit between 1 and 100 (default 20), and page with after or before.",
"doc_url": "https://api.flow.engineer/docs/errors/invalid_request",
"param": "limit"
}
}{
"error": {
"type": "authentication",
"message": "No valid API key was given.",
"hint": "Send the header Authorization: Bearer fk_test_... (or fk_live_...); no key yet? Get a test key with curl -X POST https://api.flow.engineer/v1/sandbox/keys",
"doc_url": "https://api.flow.engineer/docs/errors/authentication"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}{
"error": {
"type": "new_contact_limit",
"message": "This sender has started its 15 new conversations for today.",
"hint": "Retry after 3600 seconds; replies into existing conversations still go.",
"doc_url": "https://api.flow.engineer/docs/errors/new_contact_limit",
"retry_after": 3600,
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}Authorizations
An API key of one app, sent as Authorization: Bearer <key>. Keys start with
fk_test_ (test mode: sandbox senders and test data only) or fk_live_
(live mode). Keep live keys on your server; never ship them in an app or page.
Headers
A unique string (up to 255 characters) that makes this request safe to retry. A repeat with the same key within 24 hours returns the first answer instead of acting again. See "Idempotency" in the introduction.
1 - 255The API version to use, as a date. Without it, the version pinned to your app when it was created is used.
"2026-11-01"
Body
A request for a dedicated sender.
A messaging channel.
telegram, whatsapp, imessage The name contacts should see.
64WhatsApp and iMessage. ISO 3166-1 alpha-2 country for the number, for example IN.
^[A-Z]{2}$Telegram only, and required there. The bot token from BotFather. Stored encrypted; never returned.
128^[0-9]{1,20}:[A-Za-z0-9_-]{20,100}$Response
The sender is connected and active (Telegram bots), or an already connected bot's token was updated in place.
What your agent talks from: a Telegram bot, a WhatsApp number or an iMessage
line. shared senders are Flow's sandbox, used by many apps in test mode;
dedicated senders are yours alone. Each sender has its own limits and
warm-up state.
A sender ID, snd_ and a ULID.
^snd_[0-9A-HJKMNP-TV-Z]{26}$"snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
A messaging channel.
telegram, whatsapp, imessage shared (the sandbox) or dedicated (yours).
shared, dedicated false for sandbox senders, true for dedicated senders used in live mode.
pending: requested, being provisioned.active: sending normally.warming_up: active, with a new-contact budget that grows day by day.throttled: the gate slowed it after an abuse signal; it recovers by itself.flagged: the channel or Flow flagged it; starts are paused. A Telegram bot is alsoflaggedwhen Telegram rejects its token (revoked in @BotFather): it then sends nothing, new sends answer403 permission, and queued messages wait until you connect the bot again with its new token (POST /v1/senders). They wait at most 72 hours after Flow accepted them; older ones fail withoutside_window(channel_codequeued_too_long) inmessage.failedinstead of going out late.banned: it cannot send or receive: the channel banned it, you disconnected it (DELETE /v1/senders/{sender_id}), or its bot or line was connected to another app.
pending, active, warming_up, throttled, flagged, banned How contacts reach the sender. Which fields are set depends on the channel.
Show child attributes
Show child attributes
The sender's current budget for starting conversations.
Show child attributes
Show child attributes
When the sender was created.
The name contacts see, where the channel shows one.
With status throttled, when starts are allowed again. Recovery is automatic.
WhatsApp only. Meta's quality rating for the number.
green, yellow, red, unknown Shared senders only. The whole message a contact sends to this sender to join your app: join , a space, then the app's sandbox_join_code (from GET /v1/app), for example join brave-otter-40718263. Show it to testers as is.
"join brave-otter-40718263"