curl --request POST \
--url https://api.flow.engineer/v1/conversations/{conversation_id}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"content": {
"type": "text",
"text": "Yes, we deliver to 560103. Delivery takes 2 days."
}
}
'{
"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"
}{
"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": "unsupported_content",
"message": "iMessage cannot show buttons.",
"hint": "Set \"fallback\": \"auto\" to send numbered text instead, or send text.",
"doc_url": "https://api.flow.engineer/docs/errors/unsupported_content",
"param": "content.type"
}
}{
"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>"
}
}Send a message into a conversation
Sends one piece of content into an existing conversation. This is the normal
way to reply to a contact. The send passes the send gate (window rules,
pacing) and is queued; the answer is the queued message. Its progress arrives
as message.sent, message.delivered, message.read or message.failed
events. Messages in one conversation go out in the order they were accepted,
one at a time.
curl --request POST \
--url https://api.flow.engineer/v1/conversations/{conversation_id}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"content": {
"type": "text",
"text": "Yes, we deliver to 560103. Delivery takes 2 days."
}
}
'{
"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"
}{
"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": "unsupported_content",
"message": "iMessage cannot show buttons.",
"hint": "Set \"fallback\": \"auto\" to send numbered text instead, or send text.",
"doc_url": "https://api.flow.engineer/docs/errors/unsupported_content",
"param": "content.type"
}
}{
"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"
Path Parameters
The conversation's ID.
A conversation ID, conv_ and a ULID.
^conv_[0-9A-HJKMNP-TV-Z]{26}$"conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
Body
One piece of content to send into a conversation.
Text, both ways. With format markdown, Flow renders it in each channel's own formatting; a channel without formatting needs fallback.
- 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
- Option 14
- Option 15
Show child attributes
Show child attributes
Send this as a reply to an earlier message in the same conversation.
Telegram and iMessage show it as an inline reply; channels without inline
replies send it as a normal message. Refused with not_found (param
reply_to) when the target is not a message in this conversation, and with
invalid_request (param reply_to) when the content is typing, read,
reaction, edit or unsend, or when the target never reached the
channel (for example, it failed). If the target fails after the send was
accepted, the reply goes out as a normal message.
^msg_[0-9A-HJKMNP-TV-Z]{26}$"msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2"
What to send when the channel cannot show content. Without it, such a send
fails with unsupported_content; nothing is converted silently.
"auto": use the documented default for the content type (markdown becomes plain text, buttons become numbered text whose replies are matched back to the button IDs, a voice note becomes an audio file, a location becomes a maps link, a contact card becomes text, an effect becomes plain text, a reaction becomes the closest tapback or is skipped).- A
Contentobject: send exactly this instead.
The message's delivered_as reports what was used.
auto Unstable escape hatch. Extra channel parameters for how a message looks
and notifies, for example {"parse_mode": "HTML"} on Telegram. Each channel
has an allowlist: keys not on it are dropped, never passed to the channel, and
the typed request always wins over a key that sets the same thing. Who
receives the message and what it says always come from the typed request.
Keys starting with _flow_ are reserved and refused with invalid_request.
Flow does not validate the values, and they may break when a channel
changes. Prefer typed content.
- Telegram (Bot API parameters, on every send):
parse_mode,entities,caption_entities,link_preview_options,disable_web_page_preview,show_caption_above_media,disable_notification,protect_content,allow_paid_broadcast,message_effect_id,has_spoiler,supports_streaming,duration,width,height,performer,horizontal_accuracy,foursquare_id,foursquare_type,google_place_id,google_place_type,vcard,is_big(reactions). - iMessage: on messages,
subject,effect,preview,reply_to_idandcontact_file; ontypingcontent,typing(how long to show it, 1 to 60 seconds). Reactions, read receipts, edits and unsends take none. - WhatsApp: none yet; the channel is not live.
{
"parse_mode": "HTML",
"disable_notification": true
}
Up to 20 string pairs of your own, kept with the object and returned unchanged. Keys up to 40 characters, values up to 500.
Show child attributes
Show child attributes
Response
The message passed the gate and is queued for the channel.
One message in or out of a conversation, with its typed content and delivery status.
A message ID, msg_ and a ULID.
^msg_[0-9A-HJKMNP-TV-Z]{26}$"msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2"
A conversation ID, conv_ and a ULID.
^conv_[0-9A-HJKMNP-TV-Z]{26}$"conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1"
in from the contact, out from your app.
in, out received: inbound from the contact.queued: accepted by the gate, waiting for its turn in the conversation.sent: the channel accepted it.delivered: it reached the contact's device, where the channel reports this.read: the contact read it, where the channel reports this.failed: it could not be sent; seeerror.unsent: you unsent it.
received, queued, sent, delivered, read, failed, unsent Text, both ways. With format markdown, Flow renders it in each channel's own formatting; a channel without formatting needs fallback.
- 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
- Option 14
- Option 15
Show child attributes
Show child attributes
Whether the message belongs to live mode.
When Flow received (inbound) or accepted (outbound) the message.
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
The details of an error.
Show child attributes
Show child attributes
The channel's own ID for the message, once the channel accepted it.
The Flow ID of the message this one replies to. For a sent message, the
message the send named in reply_to. For a received message, the message it
replies to inline (a Telegram reply; on iMessage, the thread's root message).
Absent when the message is not a reply or the quoted message is not known
to Flow.
^msg_[0-9A-HJKMNP-TV-Z]{26}$"msg_01JB8ZD4M6P8R0T2V4X6Z8B0C2"
Up to 20 string pairs of your own, kept with the object and returned unchanged. Keys up to 40 characters, values up to 500.
Show child attributes
Show child attributes
When the status last changed.