curl --request GET \
--url https://api.flow.engineer/v1/stream \
--header 'Authorization: Bearer <token>'const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.flow.engineer/v1/stream', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/stream"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v1/stream"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"type": "event",
"event": {
"id": "evt_01JB8ZE5N7Q9S1V3X5Z7B9D1E3",
"created_at": "2023-11-07T05:31:56Z",
"app": "app_01JB8Z0A1C3E5G7J9K1M3P5R7T",
"livemode": true,
"timing": {
"received_at": "2023-11-07T05:31:56Z",
"stored_at": "2023-11-07T05:31:56Z",
"delivered_at": "2023-11-07T05:31:56Z"
},
"type": "message.received",
"data": {
"message": {
"id": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"direction": "in",
"status": "received",
"content": {
"type": "text",
"text": "<string>",
"format": "plain"
},
"livemode": true,
"created_at": "2023-11-07T05:31:56Z",
"delivered_as": {
"type": "text",
"reason": "<string>"
},
"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>"
},
"channel_message_id": "<string>",
"reply_to": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2",
"metadata": {},
"updated_at": "2023-11-07T05:31:56Z"
}
},
"conversation": {
"id": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"channel": "telegram",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"contact": "ct_01JB8ZB2J4K6N8Q0S2V4W6Y8A0",
"window_open_until": "2023-11-07T05:31:56Z"
}
}
}{
"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": "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>"
}
}Open the live event stream (WebSocket)
Upgrades to a WebSocket that pushes your app’s events as they happen, in log
order. Pass after to resume from the last event you saw: the stream first
replays everything after it, then goes live, so a reconnect never loses an
event.
Every WebSocket text frame is one JSON object (StreamFrame). The server
sends event frames; the client may send send frames with the same bodies
as the HTTP send endpoints, and gets one ack or error frame back for each,
matched by ref. When the server is about to restart it sends a reconnect
frame; reconnect with after set to the last event you received.
Authenticate with the Authorization header where your WebSocket client can
set headers. Where it cannot (a browser’s WebSocket, and Node’s global
WebSocket on the server), offer the key as a WebSocket subprotocol instead:
offer both flow and flow.key.<api key>, for example
new WebSocket("wss://api.flow.engineer/v1/stream", ["flow", "flow.key." + key]).
This works from server-side clients as well as browsers. The server selects
flow and never echoes the key. Offering the key protocol without flow is
refused with a plain 400 invalid_request answer, without an upgrade. When an Authorization header is present it
takes precedence. Keys are never accepted in the query string, since URLs end
up in logs. A key used in a browser is visible to whoever uses that page: do
this only for internal tools or with test keys (fk_test_).
Refusals arrive on the socket. Many WebSocket clients (Node’s and
browsers’ among them) cannot read the HTTP status of a refused upgrade, so
a WebSocket request that Flow refuses (a missing, unknown, revoked or
expired key, a bad parameter, too many streams for the key, a restart) is
still upgraded: the server sends one error frame with the usual error
body (type, message, hint, retry_after, channel_code, …), then
closes with an application close code of 4000 plus the HTTP status the
error has elsewhere, and a short reason:
4401:authentication. The key is missing, malformed, unknown, revoked or expired (channel_codesandbox_key_expired). Stop reconnecting until you have a working key.4403:permission. The key may not open this stream. Stop reconnecting.4400:invalid_request, for example a badafterortype. Fix the request; reconnecting unchanged fails again.4429:rate_limited, for example more open streams than the key may hold. Reconnect afterretry_afterseconds.4500,4503: Flow could not open the stream just now, or is restarting. Reconnect afterretry_afterseconds with the sameafter.
An open stream re-checks its key about once a minute: when the key is
revoked, the stream sends an authentication error frame and closes with
4401 too. Other closes: 1012 after a reconnect frame (reconnect at
once with after), 1008 after too many rate-limited send frames in a
row, 1001 when pings go unanswered, 1011 when the event log is
unavailable; reconnect with after after any of these. A request without
a WebSocket upgrade gets the same errors as plain HTTP answers.
curl --request GET \
--url https://api.flow.engineer/v1/stream \
--header 'Authorization: Bearer <token>'const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.flow.engineer/v1/stream', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/stream"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v1/stream"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"type": "event",
"event": {
"id": "evt_01JB8ZE5N7Q9S1V3X5Z7B9D1E3",
"created_at": "2023-11-07T05:31:56Z",
"app": "app_01JB8Z0A1C3E5G7J9K1M3P5R7T",
"livemode": true,
"timing": {
"received_at": "2023-11-07T05:31:56Z",
"stored_at": "2023-11-07T05:31:56Z",
"delivered_at": "2023-11-07T05:31:56Z"
},
"type": "message.received",
"data": {
"message": {
"id": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"direction": "in",
"status": "received",
"content": {
"type": "text",
"text": "<string>",
"format": "plain"
},
"livemode": true,
"created_at": "2023-11-07T05:31:56Z",
"delivered_as": {
"type": "text",
"reason": "<string>"
},
"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>"
},
"channel_message_id": "<string>",
"reply_to": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2",
"metadata": {},
"updated_at": "2023-11-07T05:31:56Z"
}
},
"conversation": {
"id": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"channel": "telegram",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"contact": "ct_01JB8ZB2J4K6N8Q0S2V4W6Y8A0",
"window_open_until": "2023-11-07T05:31:56Z"
}
}
}{
"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": "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
The API version to use, as a date. Without it, the version pinned to your app when it was created is used.
"2026-11-01"
Query Parameters
An item ID. Returns the items that come after it in the list's order.
64Only events of these types. Repeat the parameter for several.
20message.received: the contact sent something with content (a button tap arrives asbutton_replycontent).message.sent,message.delivered,message.read,message.failed: the status of your outbound messages.message.failedcarries the error indata.message.error.reaction.added,reaction.removed: the contact reacted to a message.typing.started,typing.stopped: the contact is typing, where the channel reports it.conversation.started: the first inbound message from a new contact, or a sandbox join (Flow itself answers the join; the join message is not amessage.received).conversation.window_closing: WhatsApp only, opt-in. The 24-hour window closes in 1 hour.sender.status_changed: a sender was throttled, flagged, banned or restored, or its WhatsApp quality rating changed.template.status_changed: Meta approved, rejected or paused a template.
message.received, message.sent, message.delivered, message.read, message.failed, reaction.added, reaction.removed, typing.started, typing.stopped, conversation.started, conversation.window_closing, sender.status_changed, template.status_changed Response
Switching to the WebSocket protocol. The schema below describes each JSON
frame on the socket, in either direction. A refused WebSocket request is
upgraded too, then answered with one error frame and a 4000-range close
code (see the description); the 4xx answers below are what a request
without an upgrade gets.
- Option 1
- Option 2
- Option 3
- Option 4
- Option 5
- Option 6
One JSON frame on the /v1/stream WebSocket. The server sends event, ack,
error and reconnect; the client sends send and start.
Always event.
event One entry in your app's log. type says what happened and selects the shape
of data. conversation is set for every event that happened in a
conversation (all but sender.status_changed and template.status_changed).
- Option 1
- Option 2
- Option 3
- Option 4
- Option 5
- Option 6
- Option 7
- Option 8
- Option 9
- Option 10
- Option 11
- Option 12
- Option 13
Show child attributes
Show child attributes