Send Text Messages
SendSeven lets you send text messages to existing conversations or directly to contacts on any supported channel. This guide covers both approaches with full examples.
Required Scopes
| Scope | Purpose |
|---|---|
messages:create | Send messages to conversations |
messages:create | Send messages to contacts |
Send a Message to a Conversation
If you already have a conversation ID (from a webhook event or the Conversations API), you can send a message directly into it.
POST /api/v1/messages
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
conversation_id | string | Yes | Target conversation ID |
text | string | Yes | Message text content |
message_type | string | Yes | Must be text |
curl
curl -X POST "https://api.sendseven.com/api/v1/messages" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "conv_8f3a2b1c",
"text": "Your order #1234 has been shipped! Tracking number: TR-98765.",
"message_type": "text"
}'
Python
import requests
BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}
response = requests.post(
f"{BASE_URL}/messages",
headers=HEADERS,
json={
"conversation_id": "conv_8f3a2b1c",
"text": "Your order #1234 has been shipped! Tracking number: TR-98765.",
"message_type": "text",
},
)
message = response.json()
print(f"Message sent: {message['id']} - Status: {message['status']}")
JavaScript
const BASE_URL = "https://api.sendseven.com/api/v1";
const HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
};
const response = await fetch(`${BASE_URL}/messages`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
conversation_id: "conv_8f3a2b1c",
text: "Your order #1234 has been shipped! Tracking number: TR-98765.",
message_type: "text",
}),
});
const message = await response.json();
console.log(`Message sent: ${message.id}`);
Response
{
"id": "msg_c3d4e5f6",
"conversation_id": "conv_8f3a2b1c",
"direction": "outbound",
"text": "Your order #1234 has been shipped! Tracking number: TR-98765.",
"message_type": "text",
"status": "pending",
"created_at": "2026-02-10T15:10:00Z"
}
Send a Message Without a Conversation ID
You can also send messages without an existing conversation ID. SendSeven automatically creates or resumes the appropriate conversation. The POST /api/v1/messages endpoint supports three resolution modes:
POST /api/v1/messages
Mode 1: Send via Contact Method ID (Cleanest)
If you know the contact method ID (from the Contacts API), this is the simplest way to send. The system resolves the recipient and channel automatically.
| Field | Type | Required | Description |
|---|---|---|---|
contact_method_id | string | Yes | Contact method ID (resolves recipient and channel) |
text | string | Yes | Message text content |
message_type | string | No | Default: text |
curl -X POST "https://api.sendseven.com/api/v1/messages" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_method_id": "cm_a1b2c3d4",
"text": "Your appointment is confirmed for tomorrow at 10:00 AM.",
"message_type": "text"
}'
Mode 2: Send to Contact with Channel (Explicit)
Specify the contact, channel, and optionally a recipient address. The to field is optional -- when omitted, SendSeven resolves the recipient from the contact's stored details.
| Field | Type | Required | Description |
|---|---|---|---|
contact_id | string | Yes | Target contact ID |
channel_id | string | No | Channel to send through. When omitted, SendSeven auto-selects an active channel for the contact (priority: WhatsApp, Telegram, Messenger, Instagram, SMS, Email). Pass it to disambiguate when the contact is reachable on several channels. |
text | string | Yes | Message text content |
message_type | string | No | Default: text |
to | string | No | Recipient address (e.g. phone number, Telegram ID, email). Resolved automatically if omitted. |
to also accepts a WhatsApp Business-scoped user ID such as US.13491208655302741918 -- pass it verbatim, periods included, together with the channel_id of the WhatsApp channel it belongs to. SendSeven recognizes the format, addresses the message correctly, and stores the value as a whatsapp_bsuid contact method on the contact. See Migrating to BSUIDs.
curl -X POST "https://api.sendseven.com/api/v1/messages" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_d4e5f6a7",
"channel_id": "ch_tg_001",
"text": "Your appointment is confirmed for tomorrow at 10:00 AM.",
"message_type": "text"
}'
Python
# Mode 1: Via contact method ID
response = requests.post(
f"{BASE_URL}/messages",
headers=HEADERS,
json={
"contact_method_id": "cm_a1b2c3d4",
"text": "Your appointment is confirmed for tomorrow at 10:00 AM.",
"message_type": "text",
},
)
message = response.json()
print(f"Sent to contact: {message['id']}")
# Mode 2: Via contact + channel
response = requests.post(
f"{BASE_URL}/messages",
headers=HEADERS,
json={
"contact_id": "contact_d4e5f6a7",
"channel_id": "ch_tg_001",
"text": "Your appointment reminder: tomorrow at 2 PM.",
"message_type": "text",
},
)
message = response.json()
print(f"Sent to contact: {message['id']}")
JavaScript
// Mode 1: Via contact method ID
const response = await fetch(`${BASE_URL}/messages`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
contact_method_id: "cm_a1b2c3d4",
text: "Your appointment reminder: tomorrow at 2 PM.",
message_type: "text",
}),
});
const message = await response.json();
console.log("Sent:", message.id);
Response
{
"id": "msg_d4e5f6g7",
"conversation_id": "conv_new_or_existing",
"direction": "outbound",
"text": "Your appointment is confirmed for tomorrow at 10:00 AM.",
"message_type": "text",
"status": "pending",
"created_at": "2026-02-10T15:20:00Z"
}
When sending without a conversation ID, SendSeven automatically finds or creates the right conversation. Use contact_method_id when possible -- it is the cleanest approach and requires the fewest fields.
Message Status Values
After sending, a message progresses through these statuses:
| Status | Description |
|---|---|
pending | Message accepted and queued for delivery (the initial status returned by POST /messages) |
sent | Message sent to the channel provider |
delivered | Delivery confirmed by the channel |
read | Message read by the recipient (if supported) |
failed | Delivery failed |
For WhatsApp, you can only send free-form text messages within a 24-hour window after the contact's last message. Outside this window, you must use an approved template.
Instagram and Messenger: human agent replies
Instagram and Facebook Messenger allow a free-form reply within 24 hours of the customer's last message. With Meta's HUMAN_AGENT message tag, a reply written personally by a human agent can still be sent up to 7 days after that message.
Opt in per message with channel_options:
{
"conversation_id": "b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e",
"text": "Sorry for the late reply, your replacement ships today.",
"channel_options": { "tag": "HUMAN_AGENT" }
}
channel_options.tag | Behaviour |
|---|---|
"HUMAN_AGENT" | 24 hours to 7 days after the customer's last message: sent with the HUMAN_AGENT tag. Within 24 hours or after 7 days: sent untagged. |
"none" | Never tagged. Accepted on every channel as a no-op. |
| omitted | API keys and OAuth tokens: never tagged. Agents signed in to the SendSeven app are tagged automatically. |
Use HUMAN_AGENT only for replies a human agent personally sends. A reply drafted with AI help counts when the agent reviews it and sends it. Never use it for automated, bot or AI messages that go out without a person sending them. Meta may revoke your messaging access for misuse.
SendSeven does not block a send because of the messaging window. Meta decides: if Meta rejects a message sent with POST /messages, it gets status failed with an explanatory error_message, and the message.failed webhook fires. A tagged message carries meta.message_tag: "HUMAN_AGENT". Setting channel_options.tag to "HUMAN_AGENT" on any other channel returns 422 channel_option_not_supported. To show the window in your own UI, read reply_window on the conversation.
Error Responses
Errors with a machine-readable code return it inside detail, under the key shown. Other errors return detail as a message string. See Error Handling for the response shapes.
| Status | detail | Description |
|---|---|---|
| 400 | "Either 'conversation_id', 'contact_method_id', or 'contact_id' must be provided." | No recipient given |
| 400 | {"error": "EMPTY_MESSAGE", "message": ...} | message_type: "text" without text and without attachments |
| 400 | {"error": "WHATSAPP_WINDOW_EXPIRED", "message": ...} | WhatsApp: the 24-hour customer service window has closed, also on Click-to-WhatsApp ad conversations. Send a template |
| 400 | {"error": "WHATSAPP_NO_CUSTOMER_MESSAGE", "message": ...} | WhatsApp: the contact has never written to you. Send a template |
| 400 | {"error": "EMAIL_SUBJECT_REQUIRED", "message": ...} | Email: no subject and the conversation has none to reuse |
| 401 | "Invalid or expired API token" | Token is invalid or expired |
| 403 | "Missing required permission(s): messages:create" | Token lacks the scope |
| 403 | {"error": "SENDER_NOT_ALLOWED", "message": ..., "sender_id": ...} | Email: you may not send from the chosen sender_id |
| 404 | "Conversation not found", "Contact not found", "Contact method not found", "Channel not found" | Unknown ID, or it belongs to another workspace |
| 409 | {"error": "CHANNEL_DISCONNECTED", "message": ..., "channel_id": ..., "channel_type": ..., "disconnection_reason": ...} | The channel is disconnected. Reconnect it in Settings > Channels |
| 409 | {"error": "CONTACT_BLOCKED", "message": ..., "contact_id": ...} | The contact is blocked |
| 422 | {"error_code": "channel_option_not_supported", "message": ..., "channel_type": ..., "option": "tag"} | channel_options.tag: "HUMAN_AGENT" on a channel other than Instagram / Messenger |
| 422 | list of field errors | Invalid request body |
RCS sends can also return 402 or 409 with {"code": ..., "message": ...}, see RCS wallet errors.
If the send fails after the request was accepted (the channel or provider rejects it, or SendSeven cannot deliver it, for example because the SMS balance is empty), the request still returns 201; the message moves to failed and the message.failed webhook fires once, after the last attempt. The rich send endpoints (interactive, contacts, sticker, carousel) send right away and report a failed send in their response; they fire message.failed once as well. They also apply the same WhatsApp 24-hour window check as POST /messages to free-form sends (interactive, contacts, sticker), with the same WHATSAPP_WINDOW_EXPIRED / WHATSAPP_NO_CUSTOMER_MESSAGE errors.
Next Steps
- Send Media Messages -- images, documents, audio, and video
- Interactive Messages -- buttons and list messages
- WhatsApp Templates -- sending template messages outside the 24h window