curl --request POST \
--url https://api.flow.engineer/v1/conversations/{conversation_id}/read \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"up_to": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({up_to: 'msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2'})
};
fetch('https://api.flow.engineer/v1/conversations/{conversation_id}/read', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/conversations/{conversation_id}/read"
payload = { "up_to": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2" }
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/conversations/{conversation_id}/read"
payload := strings.NewReader("{\n \"up_to\": \"msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2\"\n}")
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))
}{
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"action": "typing_on",
"delivered_as": {
"type": "text",
"reason": "<string>"
}
}{
"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": "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": "outside_window",
"message": "Last message from the contact was 31h ago; WhatsApp allows only templates now.",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
}
}{
"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": "channel_error",
"message": "The channel refused: the request timed out.",
"hint": "Typing and read receipts are safe to ignore; carry on and send your reply.",
"doc_url": "https://api.flow.engineer/docs/errors/channel_error",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
}
}{
"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>"
}
}Mark messages as read
Shows the contact that their messages were read, up to and including
up_to (default: the latest inbound message). Telegram bots cannot send read
receipts, so there delivered_as says it was skipped. iMessage has no
per-message read receipt: the whole conversation is marked read, and up_to
is accepted but has no effect there.
The call goes to the channel at once, so the channel’s answer is the call’s
answer. It fails with 409 outside_window when the channel’s window for
the conversation is closed, and with 502 channel_error when the channel
failed or timed out. Both are safe to ignore; never hold back a reply
because of them.
curl --request POST \
--url https://api.flow.engineer/v1/conversations/{conversation_id}/read \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"up_to": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({up_to: 'msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2'})
};
fetch('https://api.flow.engineer/v1/conversations/{conversation_id}/read', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/conversations/{conversation_id}/read"
payload = { "up_to": "msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2" }
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/conversations/{conversation_id}/read"
payload := strings.NewReader("{\n \"up_to\": \"msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2\"\n}")
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))
}{
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"action": "typing_on",
"delivered_as": {
"type": "text",
"reason": "<string>"
}
}{
"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": "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": "outside_window",
"message": "Last message from the contact was 31h ago; WhatsApp allows only templates now.",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
}
}{
"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": "channel_error",
"message": "The channel refused: the request timed out.",
"hint": "Typing and read receipts are safe to ignore; carry on and send your reply.",
"doc_url": "https://api.flow.engineer/docs/errors/channel_error",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
}
}{
"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"
Path Parameters
The conversation's ID.
A conversation ID, conv_ and a ULID.
^conv_[0-9A-HJKMNP-TV-Z]{26}$"conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
Body
Which messages to mark as read.
The latest inbound message to mark read (default the latest inbound message). On iMessage it has no effect, since the whole conversation is marked read.
^msg_[0-9A-HJKMNP-TV-Z]{26}$"msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2"
Response
The channel took the read receipt, or delivered_as says it was skipped.
The result of a typing or read call.
A conversation ID, conv_ and a ULID.
^conv_[0-9A-HJKMNP-TV-Z]{26}$"conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
Which action was taken.
typing_on, typing_off, read Present when what the contact sees differs from what you sent: the fallback
was used, or the action was skipped because the channel has no equivalent.
Absent when the content was shown as sent.
Show child attributes
Show child attributes