Skip to content
ReferenceInbox

Messages

Send and receive WhatsApp messages

DevelopersUpdated

The Messages API allows you to send various types of WhatsApp messages including text, media, templates, and interactive messages.

Retrieve messages for a specific contact.

GET/api/contacts/{id}/messages
Parameter Type Description
page integer Page number (default: 1). Page 1 is the most recent messages.
limit integer Page size, 1–100. Default 50.
before_id string Cursor — return the limit messages immediately older than this message UUID. Mutually exclusive with page.
account string Restrict to one WhatsApp account.

Messages are always returned in chronological order (oldest first) within the page.

{
"status": "success",
"data": {
"messages": [
{
"id": "uuid",
"contact_id": "uuid",
"direction": "incoming",
"message_type": "text",
"content": { "body": "Hello!" },
"media_url": "",
"media_mime_type": "",
"media_filename": "",
"interactive_data": null,
"status": "delivered",
"wamid": "wamid.xxx",
"error_message": "",
"is_reply": false,
"reply_to_message_id": null,
"reply_to_message": null,
"reactions": [],
"whatsapp_account": "Main Account",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
],
"total": 100,
"page": 1,
"limit": 50,
"has_more": true
}
}

The field is message_type (not type), the WhatsApp ID is wamid (not wa_message_id), and timestamps are created_at / updated_at (there is no timestamp). A before_id request returns messages, total and has_more only — no page/limit.

Send a text message to a contact.

POST/api/contacts/{id}/messages
{
"type": "text",
"content": {
"body": "Hello! How can I help you today?"
}
}
Field Type Required Description
type string Yes text or interactive on this endpoint. Media is sent via POST /api/messages/media.
content.body string Yes (for text) The message text
interactive object Yes (for interactive) See Send Interactive Message
reply_to_message_id string No UUID of a message to quote-reply to. Must belong to the same contact, otherwise it is silently ignored.
whatsapp_account string No Name of the WhatsApp account to send from; defaults to the contact’s account

Returns the created message object (same shape as in the list response above):

{
"status": "success",
"data": {
"id": "uuid",
"contact_id": "uuid",
"direction": "outgoing",
"message_type": "text",
"content": { "body": "Hello! How can I help you today?" },
"status": "sent",
"wamid": "wamid.xxx",
"whatsapp_account": "Main Account",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}

Send a pre-approved template message.

POST/api/messages/template
Field Type Required Description
contact_id string One of contact_id or phone_number UUID of existing contact
phone_number string One of contact_id or phone_number Phone number (creates contact if not exists)
template_name string One of template_name or template_id Name of the template
template_id string One of template_name or template_id UUID of the template
template_params object No Named or positional body parameters
header_params object No Value for a TEXT header with a {{var}} (max 1). Keyed by the variable’s name for named templates, or "1" for positional. Falls back to template_params if omitted.
button_params object No Dynamic URL button parameters (button index → value)
account_name string No Specific WhatsApp account to use

Using phone number (creates contact if needed):

Terminal window
curl -X POST "http://your-server:8080/api/messages/template" \
-H "X-API-Key: soosh_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "919876543210",
"template_name": "hello_world"
}'
{
"phone_number": "919876543210",
"template_name": "hello_world"
}

With named parameters:

If your template has placeholders like Hello {{name}}, your order {{order_id}} is ready:

{
"contact_id": "uuid",
"template_name": "order_confirmation",
"template_params": {
"name": "John",
"order_id": "12345"
}
}

With positional parameters:

{
"phone_number": "919876543210",
"template_name": "order_confirmation",
"template_params": {
"1": "John",
"2": "12345"
}
}

With a TEXT header variable:

Meta allows at most one variable in a TEXT header. If your template’s header is Our {{season}} sale is on!, supply the value via header_params:

{
"phone_number": "919876543210",
"template_name": "seasonal_promotion",
"header_params": {
"season": "Summer"
},
"template_params": {
"customer_name": "John",
"discount": "25%"
}
}

For a positional header (Our {{1}} sale is on!), use "1" as the key. If you omit header_params, the value is looked up in template_params by the same name — convenient for named templates where the header variable doesn’t collide with a body variable.

With URL button parameters:

If your template has a URL button with a dynamic variable (e.g., https://example.com/track/{{1}}), provide the dynamic value via button_params. The key is the button index (starting from "0"):

{
"contact_id": "uuid",
"template_name": "order_shipped",
"template_params": {
"name": "John",
"order_id": "12345"
},
"button_params": {
"0": "12345"
}
}
{
"status": "success",
"data": {
"message_id": "uuid",
"phone_number": "919876543210",
"status": "pending",
"template_name": "order_confirmation"
}
}

Upload and send an image, video, document, or audio message.

POST/api/messages/media
Field Type Required Description
contact_id text Yes UUID of the recipient contact
file file Yes The media file
type text No image (default), video, audio, or document
caption text No Caption text
whatsapp_account text No Account to send from; defaults to the contact’s account
Terminal window
curl -X POST "http://your-server:8080/api/messages/media" \
-H "X-API-Key: soosh_your_api_key" \
-F "contact_id=<uuid>" \
-F "type=image" \
-F "caption=Check out this product!" \
-F "file=@./product.jpg"

Meta’s own size limits apply (roughly 5 MB images, 16 MB audio/video, 100 MB documents); Soosh does not enforce its own cap.

Returns the same message object shape as the other send endpoints:

{
"status": "success",
"data": {
"id": "uuid",
"contact_id": "uuid",
"direction": "outgoing",
"message_type": "image",
"content": { "body": "Check out this product!" },
"media_url": "images/…",
"media_mime_type": "image/jpeg",
"media_filename": "product.jpg",
"status": "sent",
"whatsapp_account": "Main Account",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}

Send interactive messages with buttons, CTA URLs, WhatsApp Flows, or a Call button.

POST/api/contacts/{id}/messages

The interactive.type selects the variant:

interactive.type Fields used
button body, buttons: [{id, title}]
list body, button_text
cta_url body, button_text, url
flow flow_id (required), body, button_text (CTA label, default "Open"), header, first_screen
voice_call body, display_text, ttl_minutes

Send a message with up to 3 quick reply buttons:

Terminal window
curl -X POST "http://your-server:8080/api/contacts/{contact_id}/messages" \
-H "X-API-Key: soosh_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "interactive",
"interactive": {
"type": "button",
"body": "How would you like to proceed?",
"buttons": [
{ "id": "yes", "title": "Yes" },
{ "id": "no", "title": "No" }
]
}
}'
{
"type": "interactive",
"interactive": {
"type": "button",
"body": "How would you like to proceed?",
"buttons": [
{ "id": "yes", "title": "Yes" },
{ "id": "no", "title": "No" }
]
}
}

Send a message with a call-to-action URL button:

Terminal window
curl -X POST "http://your-server:8080/api/contacts/{contact_id}/messages" \
-H "X-API-Key: soosh_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": "Click below to view your order details",
"button_text": "View Order",
"url": "https://example.com/orders/12345"
}
}'
{
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": "Click below to view your order details",
"button_text": "View Order",
"url": "https://example.com/orders/12345"
}
}
{
"type": "interactive",
"interactive": {
"type": "flow",
"flow_id": "<meta_flow_id>",
"header": "Order form",
"body": "Tap below to place your order",
"button_text": "Open form",
"first_screen": "WELCOME"
}
}

The flow must belong to your organization, otherwise 400 Flow not found for this organization. Omitting flow_id returns 400 flow_id is required to send a flow.

{
"type": "interactive",
"interactive": {
"type": "voice_call",
"body": "Need help? Call us.",
"display_text": "Call support",
"ttl_minutes": 60
}
}

Requires the sending account to have Business Calling enabled, otherwise the request is rejected with 400. When the recipient taps the button, the resulting inbound call is sticky-routed back to the agent who sent it — the routing payload is stamped server-side from the authenticated user and is never read from the request body.

Returns the created message object. interactive_data carries the rendered interactive payload.

{
"status": "success",
"data": {
"id": "uuid",
"contact_id": "uuid",
"direction": "outgoing",
"message_type": "interactive",
"content": { "body": "How would you like to proceed?" },
"interactive_data": { },
"status": "sent",
"is_reply": false,
"whatsapp_account": "Main Account",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}

Add or remove an emoji reaction on a specific message.

POST/api/contacts/{id}/messages/{message_id}/reaction
Field Type Required Description
emoji string Yes The emoji to react with. Send an empty string ("") to remove your existing reaction.
{
"emoji": "👍"
}

Each user may have only one reaction per message; sending a new emoji replaces the previous one from the same user.

{
"status": "success",
"data": {
"message_id": "uuid",
"reactions": [
{
"emoji": "👍",
"from_user": "user-uuid"
}
]
}
}

Mark all incoming messages from a contact as read. When the account has auto read receipts enabled, read receipts are also sent to WhatsApp.

POST/api/contacts/{id}/mark-read
{
"status": "success",
"data": {
"status": "ok"
}
}
PUT/api/messages/{id}/read

Translate a message’s text (or a media caption) into one of the app’s languages with AI, for the agent to read. Nothing is sent to the customer.

Terminal window
POST /api/messages/{id}/translate
{ "to": "en" }

to is one of en, hi, ta, ar, es, pt. Requires chat:read and access to the message’s contact (agents only their own chats; an agent limited to the current conversation only its messages). The workspace needs the AI translation component and AI replies and knowledge allocated. Each translation is metered as AI usage (translate), at most 300 per agent per hour by default. A translation is kept for 30 days: the same text in the same language comes back from that cache ("cached": true) without a new AI call or charge. Card, account and ID numbers, and codes, PINs and passwords named in any of the app’s languages, are removed before the text is sent; one-time code messages are never translated.

{
"status": "success",
"data": { "text": "Hello, where is my order?" }
}
Status When
400 to isn’t a supported language, the message has no text, or it is over 2,000 characters
402 The wallet has no credit left for AI, or the monthly AI limit is reached
403 AI translation (or AI) is off for the workspace
404 Message not found, or not one the agent may read
429 Too many AI requests; try again later
502 The AI’s answer was cut off or empty (not charged); try again
503 AI isn’t available right now

GET /api/ai/status says "translate": true when the member can translate: the workspace has it and AI on, and the member has chat:read.

Rewrite the agent’s draft reply with AI before they send it. Nothing is sent: the agent reads the new text and sends it as usual.

Terminal window
POST /api/ai/rewrite
{ "text": "ur order ships tmrw", "style": "friendly" }

style is fix (spelling and grammar only), friendly, formal or shorter. The text is 1 to 2,000 characters; the rewrite keeps its language, meaning, names, numbers, links and WhatsApp formatting. Requires chat:write; the workspace needs the AI rewrite component and AI replies and knowledge allocated. Each rewrite is metered as AI usage (rewrite), at most 120 per agent per hour by default. Returns { "text": "..." }, with the same errors as translation (400 for an unknown style, or a text that is empty or too long).

GET /api/ai/status says "rewrite": true when the member can rewrite: the workspace has it and AI on, and the member has chat:write.

Stream the media attached to a message. Returns the raw file bytes with the stored MIME type — not a JSON envelope.

GET/api/media/{message_id}

Returns 404 No media found when the message has no attachment. Callers without contacts:read may only fetch media for contacts assigned to them, or for contacts with an active transfer to a team they belong to — otherwise 403 Access denied.

Messages go through the following status flow:

Status Description
pending Message queued for sending
sent Message sent to WhatsApp servers
delivered Message delivered to recipient’s device
read Message read by recipient
failed Message failed to send
received Set on inbound messages

Valid message_type values are text, image, video, audio, document, template, interactive, flow, reaction, location, and contact.

Text

Plain text messages

Image

JPEG, PNG images with optional caption

Video

MP4 videos with optional caption

Document

PDF, Word, Excel, and other documents

Audio

Voice messages and audio files

Template

Pre-approved message templates

Interactive

Buttons, lists, and reply buttons

Flow

WhatsApp Flows