Messages
Send text, media, templates and interactive messages; read a message, its delivery history and its files.
Send a message
POST /v2/messages · scope messages:write · requires Idempotency-Key · live, unpinned key
Choose where (exactly one of to, contactId, conversationId) and what (text, plus at
most one of mediaId, template, interactive):
| Field | |
|---|---|
connectionId | The connection to send from. Required. With conversationId it must be that conversation's connection (400 otherwise). |
to | A recipient: a phone number in E.164 (+447700900123) for WhatsApp, an email address for email. Reuses the person's open thread on this connection, wakes a resolved or snoozed one, or opens a new one. |
contactId | An existing contact, reached through their identity on this connection. |
conversationId | Reply into an existing conversation. |
text | The message text, up to 4,096 characters. With mediaId it is the caption. |
mediaId | A file uploaded with POST /v2/media. |
template | An approved WhatsApp template: { "id": "…" } or { "name": "…", "language": "en_US" }, plus variables by number ({ "1": "Layla" }). See Templates. |
interactive | Up to 3 buttons ({ "type": "buttons", "body", "buttons": [{ "id", "title" }] }) or a list ({ "type": "list", "body", "button", "sections": [{ "title", "rows": [{ "id", "title", "description" }] }] }), where the connection supports them. |
replyTo | Quote one of this thread's messages (its id). |
metadata | Your own labels, up to 20 string keys (40 characters) and values (500 characters). Stored on the message and returned with it. |
curl https://app.gauge.chat/backend/v2/messages \
-H "Authorization: Bearer $GAUGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3a7c0e52-61b9-4d1f-8e2a-9c5b7d3f1e08" \
-d '{
"conversationId": "c41e7b2a-5d6f-4e8a-9b1c-3d5e7f9a1b2c",
"connectionId": "9b2f6c1e-3d4a-4e5b-8f70-1a2b3c4d5e6f",
"text": "Thanks, Layla. Your refund is on its way.",
"metadata": { "orderId": "A-1042" }
}'201 sent, or 202 queued
| Status | Means | status in the body |
|---|---|---|
201 Created | The channel accepted the message during your request. | sent |
202 Accepted | The message is saved and will be dispatched in the background (WhatsApp, email, …). It is not sent yet. | queued |
Both answer the same body:
{
"message": {
"id": "7f3e9a1c-2b4d-4e6f-8a0b-1c2d3e4f5a6b",
"conversationId": "c41e7b2a-5d6f-4e8a-9b1c-3d5e7f9a1b2c",
"type": "message",
"direction": "outbound",
"authorType": "automation",
"authorMemberId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"kind": "text",
"text": "Thanks, Layla. Your refund is on its way.",
"status": "queued",
"failureReason": null,
"media": null,
"template": null,
"via": "api",
"replyToMessageId": null,
"metadata": { "orderId": "A-1042" },
"createdAt": "2026-10-21T09:30:12.418Z"
},
"conversationId": "c41e7b2a-5d6f-4e8a-9b1c-3d5e7f9a1b2c",
"status": "queued",
"warnings": []
}A 202 is a promise to try, not a delivery. Follow a queued message to sent (or failed) with the
message.status_updated webhook, or poll
GET /v2/messages/{id}. A message still queued after 30 minutes (26 hours on WhatsApp Web, which
may hold a send for its daily cap) is marked failed. Retry a timed-out request with the same
Idempotency-Key: it can never send twice (see Idempotency).
WhatsApp Web connections add "warnings": ["whatsapp_web_unofficial"] to every send: see
WhatsApp Web. A WhatsApp Web number that is still warming up caps how many
messages it sends, new chats included; over the cap a send answers 429.
Message someone first
With to or contactId instead of conversationId, a send opens the conversation if there is none
(or reuses and reopens the one there is). On WhatsApp Cloud the first message must be an
approved template; WhatsApp allows nothing else:
curl https://app.gauge.chat/backend/v2/messages \
-H "Authorization: Bearer $GAUGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d2b8e14-7a3c-4f9e-b1d6-0e8c2a4f6b39" \
-d '{
"connectionId": "9b2f6c1e-3d4a-4e5b-8f70-1a2b3c4d5e6f",
"to": "+447700900123",
"template": { "name": "appointment_reminder", "language": "en_GB", "variables": { "1": "Layla", "2": "Tuesday 22 October", "3": "10:30" } }
}'The answer carries the new conversationId. See
Start a conversation with a template.
Read a message
GET /v2/messages/{id} · scope messages:read
The message object above. status is one of queued, sent, delivered, read, failed
(outbound) or received (inbound). via is api for a message an API key sent; such a message has authorType: automation and
authorMemberId set to the key's member. media is
{ mime, filename, mediaId } when it carries a file (mediaId only for a file you uploaded).
Download a message's file
GET /v2/messages/{id}/attachments/{index} · scope messages:read
A signed download link for a file on a message, one a customer sent or one you sent, valid for
5 minutes. This is the path a webhook's media[].url carries. A message carries one file today, so
index is 0.
{
"messageId": "7f3e9a1c-2b4d-4e6f-8a0b-1c2d3e4f5a6b",
"index": 0,
"contentType": "image/jpeg",
"filename": "receipt.jpg",
"sizeBytes": 183402,
"download": { "url": "https://…", "expiresAt": "2026-10-21T09:35:12.000Z" }
}| Answer | Why |
|---|---|
404 not_found | No such message for this key, no file at that index, or reason attachment_not_available: Gauge holds no copy of the file (for example a WhatsApp Web file that was never fetched) |
503 media_unavailable | File downloads are not available on this deployment yet. Retry later. |
Delivery history
GET /v2/messages/{id}/status-history · scope messages:read
{
"messageId": "7f3e9a1c-2b4d-4e6f-8a0b-1c2d3e4f5a6b",
"status": "delivered",
"history": [
{ "status": "queued", "occurredAt": "2026-10-21T09:30:12.418Z", "failureReason": null },
{ "status": "sent", "occurredAt": "2026-10-21T09:30:13.020Z", "failureReason": null },
{ "status": "delivered", "occurredAt": "2026-10-21T09:30:15.771Z", "failureReason": null }
]
}A message moves queued → sent → delivered → read, or ends failed with a
failureReason. Not every channel reports every step (email has no read).
When a send is refused
| Answer | reason | Why |
|---|---|---|
400 validation_failed | A missing or conflicting field, an unknown template, or a conversation on another connection; details names the field | |
400 validation_failed | media_not_uploaded | The mediaId exists but its file was never PUT |
403 forbidden | sandbox_key_not_supported, key_pinned_to_projects | Test keys and App-pinned keys cannot send |
404 not_found | The connection, contact or conversation does not exist or is not visible to this key | |
409 idempotency_mismatch | The Idempotency-Key was used with a different body | |
409 conflict | idempotency_in_flight | The first request with this key is still running |
413 payload_too_large | file_too_large | The file is over the connection's limit for its type (details.maxBytes) |
422 unsupported_channel_action | outside_24h_window | Free text to WhatsApp more than 24 hours after the customer's last message, or to someone on WhatsApp Cloud who has no conversation yet: send a template |
422 unsupported_channel_action | template_not_approved, template_language_not_found, template_variable_mismatch | The template is not approved, not in that language, or variables is not exactly its {{1}}..{{n}} (Templates) |
422 unsupported_channel_action | outside_session_window | The same, on Instagram or Messenger (their own reply window) |
422 unsupported_channel_action | send_not_supported, media_not_supported, templates_not_supported, interactive_not_supported, quoted_reply_not_supported, cannot_start_conversations, connection_inactive | The connection cannot do this (see its capabilities on GET /v2/connections/{id}) |
429 rate_limited | whatsapp_web_minute_cap, whatsapp_web_daily_cap | A WhatsApp Web number's warm-up cap; wait Retry-After seconds (caps) |
429 rate_limited | Your key's or workspace's request rate; see Rate limits |