Sending messages
Everything is sent with one request, POST /v1/messages. It always needs instance_id and who it goes to: to, a phone in international format (like 201012345678), or chat_id for a group or channel. Then add one kind of content below.
The answer is immediate: 202 with status: "queued" (or "scheduled"). The message then goes out at a person's pace, and its status changes arrive as message.status webhooks. A message may wait, or fail with a reason: Waiting, errors and retries says when, for how long, and what to retry.
Text
{"instance_id": "…", "to": "201012345678", "text": "Hi Sara, your order 1042 is on its way."}
Up to 4,096 characters. WhatsApp shows a preview of the first link in the text, as in the app; add "link_preview": false to leave it out. Links are previewed only when they point to public addresses.
Images, documents, video and voice notes
Put the file in media, by address or as base64. text becomes its caption.
{"instance_id": "…", "to": "201012345678", "text": "Your ticket",
"media": {"url": "https://example.com/ticket.pdf", "mimetype": "application/pdf", "filename": "ticket.pdf"}}
{"instance_id": "…", "to": "201012345678",
"media": {"data": "JVBERi0xLjQK…", "mimetype": "application/pdf", "filename": "ticket.pdf"}}
- The
mimetypedecides how it shows:image/…as a picture,video/…as a video,audio/…as a voice note (converted to the format WhatsApp plays), anything else as a document. - By address: the
urlmust be a publichttp://orhttps://address on the usual port. We fetch the file when the message goes, following up to 3 redirects to public addresses, within 30 seconds; serve it uncompressed. If your server does not answer, or answers with an error, we try twice more over a few minutes, and then the message fails with the reason. An address inside a private network is refused with422. - As base64: put the file's content in
datainstead ofurl, for files that are not public. - Size: up to 16 MB.
- A day's files as base64: an account may send 2 GB of files as base64
datain a day, across its messages, broadcasts and statuses; 200 MB on the free trial and on the smallest plan. Past that, a request withdataanswers429with the codefile_budget_used, and itsRetry-Afterandmessagesay when the day starts again. Files byurlare not counted, so send large or many files that way. - The same rules apply to a sticker's, a status file's and a profile picture's
url(a sticker up to 1 MB; a profile picture up to 2 MB, fetched at once, within 15 seconds).
Stickers, locations, contact cards and polls
Each goes instead of text:
{"sticker": {"url": "https://example.com/sticker.webp"}}
{"location": {"latitude": 30.0444, "longitude": 31.2357, "title": "Our office"}}
{"contact": {"name": "Sara Ali", "phone": "201012345678", "organization": "Acme"}}
{"poll": {"name": "Which day suits you?", "options": ["Sunday", "Monday"], "multiple_answers": false}}
- Sticker: a WebP image of up to 1 MB, by
urlor base64data. Stickers people send you arrive withtypestickerand the image inmedia. - Poll: 2 to 12
options;multiple_answerslets people choose more than one. The message'spollshowsresults(votes per answer) andvotes(each person's latest choice), and every vote, changed vote or vote taken back arrives as amessage.poll_votewebhook. - A location's
titleand a contact card'sorganizationare optional. - None of these four can start a chat with someone who never wrote to the instance: send a short text first.
Quote, schedule, groups
- Quote a message: add
"reply_to": "<message id>", a message in the same chat. It shows as a reply in WhatsApp, andreply_tois set on the message you sent. If the quoted message never went out, or WhatsApp cannot find it, yours goes without the quote. - Send later: add
"send_at": "2026-10-01T09:00:00+03:00"(ISO 8601 with its offset; UTC without one; up to 14 days ahead). The message waits asscheduled, then goes like one sent at that moment, with every rule applied then. - A group or channel: use
"chat_id": "…@g.us"(a group) or"…@newsletter"(a channel the instance runs) instead ofto. See Chats, contacts and groups. - Someone who wrote without a phone number: WhatsApp sometimes hides a person's number, and their
message.receivedthen has"phone": nulland achat_idending in@lid. Answer them with thatchat_idinstead ofto. It works only as a reply, on the instance they wrote to: achat_idnobody wrote from is refused (422), since a first message needs a phone number. We first learn whose chat it is, so the message shows theirphoneand every rule applies to the person: if they wrote "STOP" from their number, it is not sent. When WhatsApp cannot tell us the number at that moment, the request answers503with the codeperson_unknownand aRetry-After, and nothing is sent: try again.
Retries are safe
Send an Idempotency-Key header (any unique string of yours, up to 255 characters, such as your order id). A retry with the same key on the same instance returns the first message instead of sending a second, even when both arrive at once. The retry answers 200 with the header Idempotent-Replayed: true, and the first request wins whatever the retry's body: its body is not read, so use a new key for a different message. Keys are compared exactly (Order-7 and order-7 are two keys), and each instance has its own.
We never send a second copy either: if WhatsApp did not answer a send in time, we check whether it went before trying again, and if that cannot be checked, the message fails saying it may have been delivered. Look at the chat before sending it again. A status WhatsApp did not confirm in time fails the same way, and is not posted again by itself: check the phone first. Such a message keeps its sent_at (when we handed it to WhatsApp) and counts toward the instance's limits like one that went, so a second "first message" does not follow it; it is not counted as sent or delivered in your figures.
The same key keeps returning the first message, also once it has failed. To send a failed message anew, use a new key. Which answers to retry is in Waiting, errors and retries.
After it is sent
- Follow it:
GET /v1/messages/{id}, or themessage.statuswebhooks:queued→sent→delivered→read, orfailedwith the reason inerrorand itserror_code. - List messages:
GET /v1/messages, newest first, filtered byinstance_id,phoneanddirection; followlinks.nextfor older ones. Messages are kept 30 days. - Edit:
PATCH /v1/messages/{id}withtextchanges a text the instance sent, within 15 minutes of it going out (409after that); the person sees it marked "Edited". A message that went to someone who had never written to the instance (a first message, or a follow-up to it) cannot be edited (422). - Delete for everyone:
DELETE /v1/messages/{id}on a sent message deletes it in the chat, within two days of sending (409after that). Its text and file are removed from Threadwire too. A message that went to someone who had never written to the instance cannot be deleted for everyone either (422): it is what opened the chat. - Cancel before it goes:
DELETE /v1/messages/{id}on ascheduledmessage removes it (204). On aqueuedone it cancels it (200): it stays, asfailedwitherror"Cancelled before it was sent.", and amessage.statuswebhook follows. One already handed to WhatsApp, or sent or failed meanwhile, can no longer be cancelled (409). While the instance is sending another message the answer is409withRetry-After: try again in a few seconds. Your team can do the same with "Cancel" in the chat, or in the panel's Message log. A cancelled message no longer counts against your plan's messages for the day. - React:
POST /v1/messages/{id}/reactionwith anemojireacts to a message someone sent you; an emptyemojitakes it back. The message'sreactionshows yours andreactionsthe other people's (phone,emoji), which also arrive asmessage.reactionwebhooks. A reaction notifies the person like a message, so none goes to someone who opted out, and at most 10 an hour go to one person. - Mark as read:
POST /v1/messages/{id}/readmarks the chat of a message someone sent you as read, for when your team reads it in your own system. Replying to someone who wrote in the last day does this by itself.
Reactions and read marks go only to messages someone sent you, edits and deletions only to messages the instance sent; all four need a key that may send, and together they are limited to 20 a minute per instance.
Is a phone on WhatsApp?
POST /v1/instances/{id}/check with a phone answers on_whatsapp: true or false. Use it for people who gave you their number: checking bought or guessed lists is against the Acceptable Use Policy, and WhatsApp treats many lookups of unknown numbers as scraping. So checks follow the instance's protection: a few a day while it is new, more as it warms up (checks_left_today says how many remain), at most 10 a minute, none at night or while its new chats are paused; a refusal is 429 with the reason. A "yes" is remembered for a week and a "no" for a day, so asking again is free, and a message to that person then needs no second lookup.