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.hellohop.app/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.hellohop.app/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.hellohop.app/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.hellohop.app/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.hellohop.app/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.hellohop.app/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.hellohop.app/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.hellohop.app/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 chat at or before the newest id you pass, each to its own sender. An id you pass that is already read is not marked again. When the unread range holds more than 5 senders or 500 messages, only the oldest unread messages are marked (at most 5 senders and 500 messages) and the answer carries "partial": true; the rest stays unread, so send the same request again to continue. partial is absent when everything was marked. up_to works only with message_ids.
  • Compatibility note: a read that names specific ids marks only those ids. Older messages you did not name now stay unread, where before they were hidden when a newer id was named. To clear everything up to a message, send up_to: true with that message's id, and send the same request again while partial is true.
  • A read with only chat marks the newest 500 unread messages per call. Send it again if the chat still shows unread messages.
  • 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.hellohop.app/v1/sessions/<CHANNEL_ID>/presence" \
  -H "Authorization: Bearer <CHANNEL_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"presence": "available"}'

curl -X POST "https://api.hellohop.app/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_downHelloHop 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 路 HelloHop docs