curl --request POST \
--url https://api.flow.engineer/v1/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"to": {
"phone": "+919812345678"
},
"content": {
"type": "template",
"template_id": "tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V",
"language": "en",
"params": {
"body": [
"Asha",
"order 1042"
]
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
sender: 'snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T',
to: {phone: '+919812345678'},
content: {
type: 'template',
template_id: 'tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V',
language: 'en',
params: {body: JSON.stringify(['Asha', 'order 1042'])}
}
})
};
fetch('https://api.flow.engineer/v1/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/messages"
payload = {
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"to": { "phone": "+919812345678" },
"content": {
"type": "template",
"template_id": "tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V",
"language": "en",
"params": { "body": ["Asha", "order 1042"] }
}
}
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/messages"
payload := strings.NewReader("{\n \"sender\": \"snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T\",\n \"to\": {\n \"phone\": \"+919812345678\"\n },\n \"content\": {\n \"type\": \"template\",\n \"template_id\": \"tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V\",\n \"language\": \"en\",\n \"params\": {\n \"body\": [\n \"Asha\",\n \"order 1042\"\n ]\n }\n }\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))
}{
"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>"
}
}Start a conversation
Sends the first message from one of your senders to a contact, creating the
conversation if it does not exist. Starting a conversation spends the sender’s
new-contact budget, and on WhatsApp outside the 24-hour window only a
template may be sent. If the contact already has an open conversation with
this sender, the message goes into it and spends no budget.
In test mode, to must be a contact who joined the sandbox through your app.
curl --request POST \
--url https://api.flow.engineer/v1/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"to": {
"phone": "+919812345678"
},
"content": {
"type": "template",
"template_id": "tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V",
"language": "en",
"params": {
"body": [
"Asha",
"order 1042"
]
}
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
sender: 'snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T',
to: {phone: '+919812345678'},
content: {
type: 'template',
template_id: 'tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V',
language: 'en',
params: {body: JSON.stringify(['Asha', 'order 1042'])}
}
})
};
fetch('https://api.flow.engineer/v1/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/messages"
payload = {
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"to": { "phone": "+919812345678" },
"content": {
"type": "template",
"template_id": "tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V",
"language": "en",
"params": { "body": ["Asha", "order 1042"] }
}
}
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/messages"
payload := strings.NewReader("{\n \"sender\": \"snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T\",\n \"to\": {\n \"phone\": \"+919812345678\"\n },\n \"content\": {\n \"type\": \"template\",\n \"template_id\": \"tpl_01JB8Z9X2D4F6H8K0M2P4R6T8V\",\n \"language\": \"en\",\n \"params\": {\n \"body\": [\n \"Asha\",\n \"order 1042\"\n ]\n }\n }\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))
}{
"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"
Body
The first message from one of your senders to a contact.
A sender ID, snd_ and a ULID.
^snd_[0-9A-HJKMNP-TV-Z]{26}$"snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
Who to start a conversation with: an existing contact, or an address on the
sender's channel. Give exactly one field.
Show child attributes
Show child attributes
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
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. Its conversation is the conversation it went into.
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.