Messages
Sending and receiving WhatsApp messages is what GoSendAPI is for. Everything goes through one endpoint:
POST /v1/messagesThe type field determines the shape of the rest of the payload.
Supported types
| Type | Use case | Requires template? |
|---|---|---|
text | Plain text message | Only outside 24h window |
template | Pre-approved template | Always (for outbound) |
image | JPEG/PNG image | Same as text |
video | MP4 video | Same as text |
audio | MP3/OGG audio | Same as text |
document | PDF/DOCX/XLSX | Same as text |
sticker | WebP sticker | Same as text |
location | Lat/lon + label | Same as text |
interactive | Buttons / lists / location request | Same as text |
contacts | vCard-style contact share | Same as text |
reaction | Emoji reaction to a prior message | No (always allowed) |
Plus the side-channel operation Mark as read for inbound messages.
Examples by type
text
curl https://cloud.gosendapi.com/v1/messages \
-H "X-API-Key: gsk_live_..." \
-H "Content-Type: application/json" \
-d '{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "text",
"text": {
"body": "Hola Juan, llegamos al lugar 👋"
}
}'template
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "template",
"template": {
"name": "appointment_reminder",
"language": { "code": "es_AR" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Juan" },
{ "type": "text", "text": "20/05" }
]
}
]
}
}image / video / audio / document
Two ways to attach media:
Option A — hosted URL (we download and upload to Meta):
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "image",
"image": {
"link": "https://your-cdn.com/path/to/image.jpg",
"caption": "Order #1234 receipt"
}
}Option B — Meta media ID (if you uploaded to Meta separately, e.g. for reuse):
{
"image": {
"id": "934567890123456",
"caption": "Same image, reused 100 times"
}
}For images sent once, use link. For images sent to many recipients (e.g. marketing campaigns), upload once via POST /v1/media, get an id, and reuse — saves bandwidth and is faster.
interactive — buttons
Up to 3 quick-reply buttons.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "¿Confirmás tu turno del 20/05?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "confirm_yes", "title": "Sí, confirmo" } },
{ "type": "reply", "reply": { "id": "reschedule", "title": "Reprogramar" } }
]
}
}
}When the user taps a button, you receive an inbound webhook with the id field — match it to your own action.
interactive — list
Up to 10 rows grouped in sections. Better than buttons when you have 4+ options.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "interactive",
"interactive": {
"type": "list",
"header": { "type": "text", "text": "Especialidades disponibles" },
"body": { "text": "Seleccioná una para ver horarios" },
"footer": { "text": "Centro Médico Pico" },
"action": {
"button": "Ver especialidades",
"sections": [
{
"title": "Clínica general",
"rows": [
{ "id": "spec_clinica", "title": "Clínica médica", "description": "Lun-Vie 8-18hs" },
{ "id": "spec_pediatria", "title": "Pediatría", "description": "Lun-Sab 9-13hs" }
]
},
{
"title": "Diagnóstico",
"rows": [
{ "id": "spec_eco", "title": "Ecografía", "description": "Con turno previo" }
]
}
]
}
}
}sticker
Static WebP. 512×512 px recommended.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "sticker",
"sticker": { "link": "https://your-cdn.com/sticker.webp" }
}location
Static map pin. Useful for “where to find us” / drop-off coordinates.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "location",
"location": {
"latitude": -36.6403,
"longitude": -64.2867,
"name": "Centro Médico Pico",
"address": "Av. Belgrano 123, General Pico, La Pampa"
}
}contacts
vCard-style contact share. Useful for “save our number” prompts.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "contacts",
"contacts": [
{
"name": { "formatted_name": "Centro Médico Pico", "first_name": "Centro Médico Pico" },
"phones": [
{ "phone": "+5491140123456", "type": "WORK", "wa_id": "5491140123456" }
],
"emails": [{ "email": "turnos@centropico.com.ar", "type": "WORK" }]
}
]
}interactive — location request
Ask the user to share their current location. Renders a “Send location” button in the chat.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "interactive",
"interactive": {
"type": "location_request_message",
"body": { "text": "Compartí tu ubicación para enviarte el técnico más cercano." },
"action": { "name": "send_location" }
}
}When the user taps and shares, you receive an inbound webhook with type: "location" containing latitude and longitude.
reaction
Send an emoji reaction to a previous message. Useful for acknowledging without sending text. Pass an empty emoji: "" to remove an existing reaction.
{
"phone_number_id": "555123456789",
"to": "5491140123456",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBgLNTQ5MTQwMTIzNDU2...",
"emoji": "👍"
}
}message_id is the wa_message_id of the message you want to react to (typically from a prior inbound webhook).
Mark as read
Distinct from sending — this is a side-channel operation that puts the double-blue-check on an inbound message in the customer’s WhatsApp.
POST /v1/messages/mark-read{
"phone_number_id": "555123456789",
"wa_message_id": "wamid.HBgLNTQ5MTQwMTIzNDU2..."
}Response:
{ "success": true }Idempotent — Meta accepts repeat calls. In sandbox (gsk_test_*), no real call is made and { "success": true, "sandbox": true } is returned.
Response
Live (gsk_live_* key)
A successful send returns:
{
"id": "1234567890",
"phone_number_id": "555123456789",
"direction": "outbound",
"message_type": "text",
"wa_message_id": "wamid.HBgLNTQ5...",
"status": "sent",
"to_phone": "5491140123456",
"created_at": "2026-05-18T12:34:56.789Z"
}id: GoSendAPI’s internal ID (use for follow-up queries)wa_message_id: Meta’s tracking ID (you’ll see it again in status webhook events)status: starts assent; updates todelivered,read, orfailedasync
Sandbox (gsk_test_* key)
When the API key is sandbox, the response is synthetic — no Meta call, no persistence, no webhooks:
{
"id": "sandbox-a1b2c3d4e5f6",
"phone_number_id": "555123456789",
"direction": "outbound",
"message_type": "text",
"wa_message_id": "wamid.SANDBOX.a1b2c3d4e5f6g7h8i9j0k1l2",
"status": "sent_sandbox",
"to_phone": "5491140123456",
"created_at": "2026-05-18T12:34:56.789Z",
"_sandbox": true,
"_note": "This response is synthetic. The message was NOT sent to a real WhatsApp recipient. Use a gsk_live_* key to send for real."
}The _sandbox: true flag lets your code branch (e.g. skip downstream side effects in sandbox mode).
Status updates
(Live messages only — sandbox messages don’t trigger status updates.)
WhatsApp delivers async status updates. Either poll:
curl https://cloud.gosendapi.com/v1/messages/1234567890 \
-H "X-API-Key: gsk_live_..."Or subscribe to the whatsapp.message.delivered / whatsapp.message.read / whatsapp.message.failed webhook events (recommended).
Status lifecycle:
sent ──> delivered ──> read
│
└──> failed (with error code)Listing messages
GET /v1/messages?phone_number_id=555123456789&direction=outbound&since=2026-05-01&page=1&per_page=50Filters:
| Param | Type | Description |
|---|---|---|
phone_number_id | string | Only messages on this number |
direction | inbound | outbound | Filter by direction |
status | string | sent/delivered/read/failed |
since | ISO date | From this date onwards |
until | ISO date | Up to this date |
page | int | Default 1 |
per_page | int | Default 20, max 100 |
Receiving messages (inbound)
Inbound messages arrive via webhook to your configured endpoint. Event: whatsapp.message.received.
Sample payload:
{
"event": "whatsapp.message.received",
"delivery_id": "8b3f...",
"occurred_at": "2026-05-18T12:35:00Z",
"tenant_id": "42",
"phone_number_id": "555123456789",
"message": {
"id": "wamid.HBgLNTQ5...",
"timestamp": "1747570500",
"type": "text",
"from": "5491140123456",
"text": { "body": "Confirmo el turno" },
"gosendapi": {
"direction": "inbound",
"status": "received",
"origin": "cloud_api",
"has_media": false,
"content": "Confirmo el turno"
}
},
"conversation": { "id": "...", "status": "active" },
"is_new_conversation": false
}See Webhooks guide for verification and retry policy.
Best practices
- Always send
idempotency_keyin headers for retries. Same key = same message, won’t duplicate. - Reuse media via
idfor marketing sends — way faster than re-uploading - Pre-validate phone numbers (E164 format, no spaces, no
+) before calling the API to reduce 400s - Cache
phone_number_id→customer_idin your DB to avoid joining on every event handler - Don’t poll — use webhooks for status updates. Polling 1000s of messages is wasteful and gets rate-limited