Sending messages

Send text, polls and contact cards, reply, mention, react, delete, mark read, show presence.

Media is on its own page, Media. Placeholders in examples: channel <CHANNEL_ID>, token <CHANNEL_TOKEN>, number 66XXXXXXXXX, message 3EB0C767D26A1D8E2A41, base https://API_ORIGIN/v1.

  • POST/v1/sessions/<CHANNEL_ID>/messages/text

    Send a text, with optional mentions and a quoted reply.

  • POST/v1/sessions/<CHANNEL_ID>/messages/contact

    Send a contact card.

  • POST/v1/sessions/<CHANNEL_ID>/messages/poll

    Send a poll.

  • PUT/v1/sessions/<CHANNEL_ID>/messages/<MESSAGE_ID>/reaction

    React to a message.

  • DELETE/v1/sessions/<CHANNEL_ID>/messages/<MESSAGE_ID>/reaction

    Remove your reaction.

  • DELETE/v1/sessions/<CHANNEL_ID>/messages/<MESSAGE_ID>

    Delete a message you sent, for everyone.

  • POST/v1/sessions/<CHANNEL_ID>/messages/read

    Send read receipts.

  • POST/v1/sessions/<CHANNEL_ID>/presence

    Set the channel's presence.

  • POST/v1/sessions/<CHANNEL_ID>/typing

    Show or clear the typing indicator.

What every send returns

Sends answer 200 with:

json
{
  "id": "3EB0C767D26A1D8E2A41",
  "chat_jid": "[email protected]",
  "timestamp": "2026-09-17T03:00:00Z",
  "status": "server_ack"
}
  • id identifies the message. Keep it: you need it to quote, react, delete, and to match message.status webhooks.
  • server_ack means WhatsApp's servers accepted it. Whether it reached the phone, and whether it was read, arrives as message.status webhooks (delivered, read, played, undecryptable).
  • The API has no idempotency key. If a send returns 504 send_unconfirmed, the message may have gone out. Treat it as possibly delivered and do not resend blindly. The id in that error names a log row marked ambiguous. It is not a WhatsApp message ID. See Recipes.
  • An API send is not echoed back to you as a message webhook.
  • Every send, poll, reaction and delete counts against your daily send cap, and against the sandbox allowance on a sandbox channel.

Send a text

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/text" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"to": "66XXXXXXXXX", "text": "Thanks, see you tomorrow"}'

Fields:

  • to (required): phone number with country code, or a JID.
  • text: the message.
  • mentions (optional): phone numbers or user JIDs of the people to mention. Group and other non-person addresses are refused with 400.
  • quoted (optional): the ID of a message this channel has in the same chat. The text is sent as a reply to it. Letters, digits, dot, underscore and hyphen, 1 to 128 characters.

Reply and mention in one call:

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/text" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"to": "[email protected]", "text": "@66XXXXXXXXX see above", "mentions": ["66XXXXXXXXX"], "quoted": "3EB0C767D26A1D8E2A41"}'

Put @ and the number in the text where the tag should appear. The mentions list notifies the person. Without the @number in the text, they are notified but nothing is highlighted.

Errors specific to this route: 400 quote_not_found (the quoted message is not in this channel's log), 400 quote_wrong_chat (it belongs to another chat).

Send a poll

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/poll" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"to": "66XXXXXXXXX", "question": "Which day suits you?", "options": ["Monday", "Tuesday", "Wednesday"], "multiple": false}'
  • question up to 255 characters.
  • options: 2 to 12 different options, each up to 100 characters.
  • multiple: true lets a person choose more than one option. The default is false.

Votes arrive as message webhooks of type poll_vote, with the chosen options by name. See Webhook events.

Send a contact card

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/contact" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"to": "66XXXXXXXXX", "display_name": "Customer A", "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Customer A\nTEL;type=CELL;waid=66XXXXXXXXX:+66XXXXXXXXX\nEND:VCARD"}'

to, display_name and vcard are all required.

React to a message

bash
curl -X PUT "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/3EB0C767D26A1D8E2A41/reaction" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"emoji": "馃憤"}'
  • Send exactly one emoji. The value must be at most 32 bytes of valid UTF-8, include at least one non-ASCII character, and contain no spaces, control characters, ASCII letters or digits. ok is refused with 400. The gateway does not check that the value is a single emoji, so send exactly one.
  • A new reaction replaces this channel's earlier one on that message.
  • A message this channel has no record of answers 404 message_not_found.
  • Remove your reaction with DELETE on the same path, no body.

Delete a message for everyone

bash
curl -X DELETE "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/3EB0C767D26A1D8E2A41" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>"
json
{
  "message_id": "3EB0C767D26A1D8E2A41",
  "chat_jid": "[email protected]",
  "id": "3EB0C767D26A1D8E2A42",
  "timestamp": "2026-09-17T03:01:00Z",
  "status": "server_ack"
}
  • Only your own messages, sent through the API or on the phone. Someone else's answers 403 not_own_message. Deleting other people's messages as a group admin is not offered.
  • Within 48 hours of sending. An older message answers 409 revoke_window_passed.
  • A message not in the log answers 404 message_not_found.
  • Deleting again is safe: it answers 200 with status: "already_revoked" and sends nothing. In that case id and timestamp are absent. A second delete while the first is running answers 409 revoke_in_progress.
  • The log keeps the message and marks it with revoked_at. A delete made on the phone, or by the sender of a message you received, arrives as a message webhook of type protocol with revoke.message_id.

Mark messages read

Mark a whole chat read:

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/read" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"chat": "[email protected]"}'
json
{ "status": "read", "receipted": 3 }

Or mark named messages:

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/messages/read" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"chat": "[email protected]", "message_ids": ["3EB0C767D26A1D8E2A41"], "up_to": true}'
  • Send message_ids (at most 500), or send only chat to mark the whole chat. sender is also accepted.
  • A whole-chat read that finds nothing unread answers 200 with {"status":"nothing_to_acknowledge","receipted":0}.
  • In a group, only the ids you pass are marked, and each sender sees only their own. up_to: true also marks every unread message in the group at or before the newest id you pass, each to its own sender. A request that would mark 500 or more messages this way, or send receipts to more than 5 senders, answers 400 up_to_range_too_large and sends nothing. up_to works only in a group chat and only with message_ids.
  • skipped lists ids you sent that were not marked: ids this channel has no record of, when the request does not settle which single chat they belong to.
  • If a read fails after some receipts went out, the answer is 502 receipt_partial with receipted. Do not resend the request as is.

Presence and typing

bash
curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/presence" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"presence": "available"}'

curl -X POST "https://api-wagate.socialground.co/v1/sessions/<CHANNEL_ID>/typing" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"chat": "[email protected]", "state": "typing"}'

Both answer {"status":"ok"}.

  • presence: available or unavailable. online and offline are accepted as other names.
  • state: typing (or composing), recording, or paused (or stop, which clears it).

What can go wrong on a send

Status and codeMeaningWhat to do
409 not_linkedno phone is linkedlink it in the console
409 stood_downWAgate paused the number after WhatsApp pushed backreconnect in the console once the restriction ends
403 channel_unentitledno paid slot and not the sandbox channelbuy a channel
429 sandbox_allowance_exceededsandbox allowance usedwait until resets_at
429 gateway_busy, rate_limitedback-pressurewait Retry-After, retry
429 whatsapp_rate_limitedWhatsApp throttled the numberback off, retry later
504 send_unconfirmedoutcome unknown; treat as possibly delivereddo not resend blindly; see Recipes
Sending messages 路 WAgate docs