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:
{
"id": "3EB0C767D26A1D8E2A41",
"chat_jid": "[email protected]",
"timestamp": "2026-09-17T03:00:00Z",
"status": "server_ack"
}ididentifies the message. Keep it: you need it to quote, react, delete, and to matchmessage.statuswebhooks.server_ackmeans WhatsApp's servers accepted it. Whether it reached the phone, and whether it was read, arrives asmessage.statuswebhooks (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. Theidin 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
messagewebhook. - Every send, poll, reaction and delete counts against your daily send cap, and against the sandbox allowance on a sandbox channel.
Send a text
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:
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
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}'questionup to 255 characters.options: 2 to 12 different options, each up to 100 characters.multiple:truelets a person choose more than one option. The default isfalse.
Votes arrive as message webhooks of type poll_vote, with the chosen options by name. See Webhook events.
Send a contact card
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
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.
okis 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
DELETEon the same path, no body.
Delete a message for everyone
curl -X DELETE "https://api.hellohop.app/v1/sessions/<CHANNEL_ID>/messages/3EB0C767D26A1D8E2A41" \
-H "Authorization: Bearer <CHANNEL_TOKEN>"{
"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 caseidandtimestampare absent. A second delete while the first is running answers 409revoke_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 amessagewebhook of typeprotocolwithrevoke.message_id.
Mark messages read
Mark a whole chat read:
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]"}'{ "status": "read", "receipted": 3 }Or mark named messages:
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 onlychatto mark the whole chat.senderis 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: truealso 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.partialis absent when everything was marked.up_toworks only withmessage_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: truewith that message's id, and send the same request again whilepartialistrue. - A read with only
chatmarks the newest 500 unread messages per call. Send it again if the chat still shows unread messages. skippedlists 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_partialwithreceipted. Do not resend the request as is.
Presence and typing
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:availableorunavailable.onlineandofflineare accepted as other names.state:typing(orcomposing),recording, orpaused(orstop, which clears it).
What can go wrong on a send
| Status and code | Meaning | What to do |
|---|---|---|
409 not_linked | no phone is linked | link it in the console |
409 stood_down | HelloHop paused the number after WhatsApp pushed back | reconnect in the console once the restriction ends |
403 channel_unentitled | no paid slot and not the sandbox channel | buy a channel |
429 sandbox_allowance_exceeded | sandbox allowance used | wait until resets_at |
429 gateway_busy, rate_limited | back-pressure | wait Retry-After, retry |
429 whatsapp_rate_limited | WhatsApp throttled the number | back off, retry later |
504 send_unconfirmed | outcome unknown; treat as possibly delivered | do not resend blindly; see Recipes |