New Safe Mode warms up new numbers to cut the risk of a WhatsApp ban. Learn more →

Maytapi/Blog/Guides

WhatsApp API Send Message: Every Message Type with Examples

Send text, media, voice notes, locations, contacts, polls and reactions with one sendMessage call. Request bodies, to_number formats, acks and error codes.

Guides Oct 27, 2026·9 min read

WhatsApp API Send Message: Every Message Type with Examples

Every message your code sends through Maytapi goes through one endpoint: POST /sendMessage. The type field decides what the recipient sees, whether that's a line of text, a PDF, a voice note, a poll, or a reaction to a message they sent you. This guide covers how to send WhatsApp messages with the API: each one-to-one message type with a working request body, the recipient formats to_number accepts, what the response tells you, how to follow delivery, and which errors to handle.

Maytapi connects your own WhatsApp number as a linked device, the same way WhatsApp Web does. Messages go out from that number, with no templates and no per-message fees.

The sendMessage request

Every request goes to the base URL https://api.maytapi.com/api/PRODUCT_ID/PHONE_ID/ and carries your token in the x-maytapi-key header. The product ID and token are on the console's Product ID & Token page. The phone ID is on the Phones page, or you can get it from listPhones.

A text message is the simplest case:

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/sendMessage" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to_number": "1415xxxxx67",
    "type": "text",
    "message": "Hi Ayse, your order #1042 has shipped."
  }'

The same call in Node.js 18+, wrapped in a helper that the rest of this guide reuses:

JavaScript
async function send(body) {
  const res = await fetch(
    `https://api.maytapi.com/api/${PRODUCT_ID}/${PHONE_ID}/sendMessage`,
    {
      method: "POST",
      headers: { "x-maytapi-key": TOKEN, "Content-Type": "application/json" },
      body: JSON.stringify(body)
    }
  );
  return res.json();
}

const r = await send({ to_number: "1415xxxxx67", type: "text", message: "Hello!" });
console.log(r.data.msgId);

The rest of this guide shows only the JSON body. Every example goes to the same endpoint.

Who you can send to: to_number formats

to_number accepts several formats. The API normalizes them before queuing the message:

You pass Treated as Example
Digits with country code A person 1415xxxxx67 (a leading + is accepted)
Chat id ending in @c.us A person 1415xxxxx67@c.us
Group id ending in @g.us A group 120363012345678901@g.us
Privacy id ending in @lid A person 112893567721673@lid
@ plus a WhatsApp username A person @john_doe or @john_doe:1234

Plain digits become @c.us. Digits with a dash (the old group id format, like 1415xxxxx67-1574073754) become @g.us. Newer group ids are digits only, so always include @g.us for groups. Without it, the API treats the id as a phone number. To find group ids and post to groups, see send messages to a WhatsApp group with the API.

A @lid is the privacy id WhatsApp uses when it hides a phone number, mostly in groups. You can reply to one directly. To match it to a phone number, see WhatsApp LID to phone number.

For usernames, to_number must start with @. Without it, the value is read as a phone number. If the account protects its username with a key, add the 4-digit key after a colon. You only need the key for the first message from your number. WhatsApp doesn't reveal the person's number, so the chat id on acks and webhooks is @c.us when your account already knows the number and @lid when it doesn't.

What the response means

A successful call returns right away:

JSON
{
  "success": true,
  "data": {
    "chatId": "1415xxxxx67@c.us",
    "msgId": "50d8b950-a67f-11eb-91cc-d97af631df95"
  }
}

success: true means the message was accepted and added to your phone's sending queue, not that it reached the phone. Your number works through the queue in order. chatId is the normalized recipient, and msgId is Maytapi's id for this message. Store it: you'll use it to match receipts, and you can pass it to reply_to, delete, edit, forward and the other actions below. The API translates it to the real WhatsApp message id.

When you send media as base64, the response also includes media_url, the hosted copy of your file.

To see how many messages are waiting, call GET /queue:

Terminal
curl "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/queue" \
  -H "x-maytapi-key: $TOKEN"

It returns { "success": true, "stats": { "sending": 0 } }. If the phone is disconnected, sendMessage still accepts messages. They wait in the queue and go out once the phone reconnects. A growing sending count usually means the phone needs attention. GET /purgeQueue drops everything still waiting.

Text messages and replies

JSON
{
  "to_number": "1415xxxxx67",
  "type": "text",
  "message": "Your appointment is *tomorrow at 10:00*. Reply YES to confirm."
}

WhatsApp formatting (*bold*, _italic_, ~strike~) and emojis work as usual. Preview the formatting with the text formatter.

To quote a message, add reply_to. For a message you sent, use the msgId from the send response. For a received message, use its full serialized id from your webhook, like false_1415xxxxx67@c.us_3EB0A81ECEA6D47F2F13:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "text",
  "message": "Yes, we deliver on Saturdays too.",
  "reply_to": "false_1415xxxxx67@c.us_3EB0A81ECEA6D47F2F13"
}

As a quote, reply_to works with text, media, voice notes and polls. Reactions, edits and pins also take reply_to, but there it names the target message instead of quoting it. Mentions with @number and @all are mainly a group feature. The group messaging guide covers them.

The documentation also lists optional typing and duration fields for text. Not every connection honors them, so don't build anything that depends on a typing indicator.

Images, video and documents

Every file type goes through type: "media". Put a public URL in message and an optional caption in text:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "media",
  "message": "https://example.com/images/order-1042.jpg",
  "text": "Here is your order, packed and ready."
}

The file type comes from the URL, so a .jpg goes out as an image, an .mp4 as a video and a .pdf as a document. For documents, set filename so the recipient sees a proper name:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "media",
  "message": "https://example.com/files/invoice-1042.pdf",
  "filename": "invoice-1042.pdf",
  "text": "Invoice for order #1042"
}

If the file isn't hosted anywhere, send it as a base64 data URI. The API uploads it and returns the hosted address as media_url:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "media",
  "message": "data:image/png;base64,iVBORw0KG...",
  "text": "Your QR ticket"
}

Two optional flags: isGif: true plays an MP4 as a looping GIF, and isHd: true asks for HD image quality on connections that support it. The media messages API page has more on files and formats.

Voice notes, stickers and short videos

audio sends a file as a voice note, the kind people record with the microphone button. Sending the same file with type: "media" delivers it as an attached audio file instead of a voice note bubble. OGG with the Opus codec is what WhatsApp itself uses for voice notes, so it's the safest format:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "audio",
  "message": "https://example.com/audio/welcome.ogg"
}

sticker sends an image as a sticker. Use a WebP file, as in the documentation example:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "sticker",
  "message": "https://example.com/stickers/thanks.webp"
}

shortVideo sends a video as a round video note, the circular clip WhatsApp plays inline. Keep it short, like one recorded in the app:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "shortVideo",
  "message": "https://example.com/video/hello.mp4"
}

A location pin needs latitude and longitude. text is the label under the pin:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "location",
  "text": "Our Kadikoy store",
  "latitude": "41.093292",
  "longitude": "29.061737"
}

There are two ways to share a contact. vcard sends a contact card you build yourself, with a displayName and a full vCard string:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "vcard",
  "message": {
    "displayName": "Acme Support",
    "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Acme Support\nTEL;TYPE=CELL:+1415xxxxx68\nEND:VCARD"
  }
}

contact shares an existing WhatsApp account by chat id. Pass one id as a string, or several as an array:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "contact",
  "message": ["1415xxxxx68@c.us", "905301234569@c.us"]
}

link sends a URL with a rich preview card. The URL goes in message, and the line of text that goes with it goes in text:

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "link",
  "message": "https://example.com/track/1042",
  "text": "Track your delivery here"
}

The preview's title, description and image come from the page's metadata, so pages with Open Graph tags look best.

Polls

JSON
{
  "to_number": "1415xxxxx67@c.us",
  "type": "poll",
  "message": "Which delivery slot works for you?",
  "options": ["Morning", "Afternoon", "Evening"],
  "only_one": true
}

Give at least two options. only_one: true allows a single choice; leave it out to allow several. Votes arrive on your webhook as pollVoteUpdate messages.

Act on existing messages: react, edit, delete, forward, star, pin

These types change a message that already exists. Each one points to its target with an id, either the msgId returned when you sent it or the serialized id of a received message.

Comparison of sendMessage types: new content types like text, media, location, poll and contact versus action types like reaction, edit, pinMessage, delete, forward, star and unreact that target an existing message id.

React and unreact. The emoji goes in message and the target in reply_to. To remove your reaction, send unreact with the target id in message:

JSON
{ "to_number": "1415xxxxx67@c.us", "type": "reaction", "message": "👍", "reply_to": "false_1415xxxxx67@c.us_3EB0A81ECEA6D47F2F13" }
JSON
{ "to_number": "1415xxxxx67@c.us", "type": "unreact", "message": "false_1415xxxxx67@c.us_3EB0A81ECEA6D47F2F13" }

Edit. Put the new text in message and the message you're editing in reply_to. You can only edit your own messages, and WhatsApp only allows edits for a limited time after sending:

JSON
{ "to_number": "1415xxxxx67@c.us", "type": "edit", "message": "Your order ships Monday (updated).", "reply_to": "50d8b950-a67f-11eb-91cc-d97af631df95" }

Delete. Pass the message id in message. Set deleteForEveryone explicitly: true removes the message for everyone, false only from your own chat:

JSON
{ "to_number": "1415xxxxx67@c.us", "type": "delete", "message": "50d8b950-a67f-11eb-91cc-d97af631df95", "deleteForEveryone": true }

Forward. to_number is the destination and message is the id of the message to forward. The documentation examples add forward_caption: true so media keeps its caption:

JSON
{ "to_number": "905301234569@c.us", "type": "forward", "message": "false_1415xxxxx67@c.us_3EB0A81ECEA6D47F2F13", "forward_caption": true }

Star. action: 1 stars the message in message, action: 2 unstars it.

Pin. pinMessage takes the target in reply_to, action: 1 to pin or action: 2 to unpin, and a duration of 86400, 604800 or 2592000 seconds (1, 7 or 30 days):

JSON
{ "to_number": "1415xxxxx67@c.us", "type": "pinMessage", "action": 1, "duration": 604800, "reply_to": "50d8b950-a67f-11eb-91cc-d97af631df95" }

Interactive, catalog, channel and group messages

sendMessage handles a few more families, each covered in its own guide:

Message type reference

type Required fields Example value
text message "Hello!"
media message (URL or base64), optional text, filename "https://example.com/invoice.pdf"
audio message (URL) "https://example.com/welcome.ogg"
sticker message (URL) "https://example.com/thanks.webp"
shortVideo message (URL) "https://example.com/hello.mp4"
location latitude, longitude, optional text "41.093292", "29.061737"
vcard message.displayName, message.vcard "BEGIN:VCARD\n..."
contact message (chat id or array) "1415xxxxx68@c.us"
link message (URL), optional text "https://example.com/track/1042"
poll message, options, optional only_one ["Morning", "Evening"]
reaction message (emoji), reply_to "👍"
unreact message (message id) "false_1415xxxxx67@c.us_3EB0..."
edit message (new text), reply_to "Updated text"
delete message (message id), deleteForEveryone true
forward message (message id) "50d8b950-a67f-..."
star message (message id), action 1 star, 2 unstar
pinMessage reply_to, action, duration 604800

To quote a message, add reply_to to a text, media, voice note or poll message.

Track delivery with ack webhooks

Because sendMessage only queues the message, delivery shows up later on your webhook. With ack delivery turned on (ack_delivery in setWebhook), each receipt arrives as a type: "ack" event.

Flow of a sent message: POST /sendMessage, the message is queued and a msgId returned, the phone sends it, then an ack webhook with the same msgId reports delivered, reached, seen or played, or an error event reports a code.

Each item in the event's data array has a msgId that matches the one sendMessage returned, plus an ackCode: 1 delivered (to WhatsApp's servers), 2 reached (the recipient's phone), 3 seen, 4 played (voice notes) and 5 deleted. Receipts can arrive more than once and out of order, so keep the highest ackCode per message.

A message that can't be sent arrives as a type: "error" event instead, with a code, a message, and the original request in data. Its data.id is the same msgId, so you can mark the right message as failed. The webhook events guide shows every payload and how to build a reliable handler.

Errors you should handle

Some problems are rejected right in the HTTP response:

Response message Cause
to_number is missing..! No to_number in the body
Invalid phone number to_number shorter than 10 characters (usernames excepted)
body.type is required. No type in the body
API token does not match! Please check your API token. Wrong or missing x-maytapi-key
[PHONE_ID] is not a valid phone instance id... / You dont own the phone[...] Wrong phone ID in the URL

Everything else is checked when your phone takes the message from the queue, so it comes back as an error webhook event:

code message What to do
E01 The contact cannot be found! The number isn't on WhatsApp. Check the country code, or check numbers in advance with bulk number check.
E01 Found Message Dupe. The duplicate filter dropped a repeated message (see below). It shares E01 with the row above, so branch on message as well as code.
E02 Message type '...' is invalid... Typo in type. Compare against the table above.
E03 Message is empty. message is missing or empty.
M01 Can not found forwarded message. The message to forward isn't available to your phone.
F12 This WhatsApp number is temporarily restricted... WhatsApp has limited new conversations. See ban prevention and Safe Mode.

Group sends have their own codes, such as OA2 when your number isn't a member. A phone that's disconnected doesn't produce an error: as described above, the message waits in the queue.

sendMessage has a duplicate filter that drops the same content sent to the same chat several times in a short window. It protects your number from loops in your own code. If a repeat is intentional, add "skip_filter": true to that request rather than to every message.

Next steps

You now know every one-to-one message type and the field each one needs. To build on it:

Published Oct 27, 2026 ← All posts
Start using the WhatsApp API

Get your key, send your first message in ten minutes

No approval, no templates, no card. Every endpoint is open for three days.