✅ Meta App Review approved — GoSendAPI Cloud is live. Sign up free →

Messages

Sending and receiving WhatsApp messages is what GoSendAPI is for. Everything goes through one endpoint:

POST /v1/messages

The type field determines the shape of the rest of the payload.

Supported types

TypeUse caseRequires template?
textPlain text messageOnly outside 24h window
templatePre-approved templateAlways (for outbound)
imageJPEG/PNG imageSame as text
videoMP4 videoSame as text
audioMP3/OGG audioSame as text
documentPDF/DOCX/XLSXSame as text
stickerWebP stickerSame as text
locationLat/lon + labelSame as text
interactiveButtons / lists / location requestSame as text
contactsvCard-style contact shareSame as text
reactionEmoji reaction to a prior messageNo (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 as sent; updates to delivered, read, or failed async

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=50

Filters:

ParamTypeDescription
phone_number_idstringOnly messages on this number
directioninbound | outboundFilter by direction
statusstringsent/delivered/read/failed
sinceISO dateFrom this date onwards
untilISO dateUp to this date
pageintDefault 1
per_pageintDefault 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_key in headers for retries. Same key = same message, won’t duplicate.
  • Reuse media via id for 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_idcustomer_id in 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

What’s next