Skip to Content
Sending Messages

Sending Messages

client.send(recipient) returns a chainable, lazy message builder. You add exactly one content method (.text, .image, .poll, …), optionally chain modifiers (.reply, .mentions, .disappearing, …), then await the builder to actually send. Every successful send resolves to the new message’s WAMessageKey, which you can later pass to client.edit/client.delete/client.forward.

import { Client } from 'zaileys' const client = new Client({ authType: 'qr' }) client.on('connect', async () => { const key = await client.send('6281234567890@s.whatsapp.net').text('Hello there') console.log('sent', key.id) })

The builder is a thenable: nothing is sent until you await it (or call .then). Build the message fully, then await once. There is no separate .send() call at the end.

The recipient: JID vs username

client.send(to) accepts either a fully-qualified JID or a plain phone number / username.

  • A JID is used as-is. Recognised suffixes: @s.whatsapp.net (DM), @g.us (group), plus @lid, @newsletter, @broadcast, @c.us.
  • Anything without a JID suffix is treated as a username/number and is resolved to a JID via WhatsApp’s onWhatsApp lookup at await time (the lookup is cached per client and de-duplicated for concurrent sends). If the number is not on WhatsApp, the send rejects with a USERNAME_NOT_FOUND error.
await client.send('6281234567890@s.whatsapp.net').text('explicit JID') await client.send('120363012345678901@g.us').text('to a group') await client.send('6281234567890').text('bare number — resolved automatically')
⚠️

Username resolution happens lazily when the builder is awaited, so an invalid number surfaces as a rejected promise from the await client.send(...) call, not from client.send(...) itself.

Media sources

Every media method (image, video, audio, document, sticker, and album items) accepts a MediaSource, which is one of:

SourceTypeHow it’s loaded
Remote URLstring starting with http:// / https://fetched (30s timeout)
URL objectURLhttp(s): is fetched; file: is read from disk
Local pathstringread from the filesystem
In-memory bytesBufferused directly
await client.send(to).image('https://example.com/photo.jpg') // remote URL await client.send(to).image('./assets/photo.jpg') // local path await client.send(to).image(new URL('file:///tmp/photo.jpg')) // URL object await client.send(to).image(myBuffer) // Buffer

The MIME type is auto-detected from the bytes. video() rejects with INVALID_OPTIONS if the detected MIME is not video/*; audio() and sticker() transcode the source automatically (see Media Processing). Failed loads (bad URL, missing file, non-2xx response) reject with a MEDIA_LOAD_FAILED error.


Text

The simplest message. Pass a string.

await client.send(to).text('Hello, world!')
OptionTypeDefaultDescription
richbooleanfalseRender the string as an AIRich card with markdown formatting instead of a plain text message. See Rich Responses.
await client.send(to).text('**bold** and _italic_ rendered as a card', { rich: true })

rich: true switches to the AIRich renderer (markdown, LaTeX, headers). For ordinary chat text, omit it. Full options for rich text live in Rich Responses.

Image

await client.send(to).image('./photo.jpg', { caption: 'A nice view' })
OptionTypeDefaultDescription
captionstringText shown beneath the image.
viewOncebooleanfalseSend as a view-once (disappears after the recipient opens it).

Video

await client.send(to).video('./clip.mp4', { caption: 'Watch this' })
OptionTypeDefaultDescription
captionstringText shown beneath the video.
gifPlaybackbooleanfalseLoop silently like a GIF (see below).
viewOncebooleanfalseSend as a view-once video.
⚠️

The source must decode to a video/* MIME type, otherwise the send rejects with INVALID_OPTIONS.

GIF

WhatsApp has no real GIF message type — animated “GIFs” are videos played in a silent loop. Use video() with gifPlayback: true. The source should be an MP4 (convert real .gif files first; see Media Processing).

await client.send(to).video('./animation.mp4', { gifPlayback: true, caption: 'lol' })

Audio

A regular audio file (music player UI). The source is transcoded to Opus automatically.

await client.send(to).audio('./song.mp3')
OptionTypeDefaultDescription
pttbooleantrueSend as a voice note (push-to-talk) rather than a music file.
secondsnumberautoReported duration. When omitted and ptt is on, it’s derived from the computed waveform.
⚠️

ptt defaults to true — a bare .audio(src) is sent as a voice note. Pass { ptt: false } for a regular audio/music message.

Voice note (PTT)

A voice note is just audio() with ptt: true (the default). When sent as a voice note, zaileys computes a waveform and duration so the chat shows the speech-bubble player.

await client.send(to).audio('./voice.ogg', { ptt: true }) // explicit await client.send(to).audio('./voice.ogg') // ptt:true is the default

Document

Send any file as a downloadable attachment. fileName is required.

await client.send(to).document('./report.pdf', { fileName: 'Q3-report.pdf' })
OptionTypeDefaultDescription
fileNamestringrequiredDisplay name (and extension) shown to the recipient. Must be non-empty.
mimetypestringauto-detectedOverride the auto-detected MIME type.
captionstringText shown beneath the document.
await client.send(to).document(docBuffer, { fileName: 'notes.txt', mimetype: 'text/plain', caption: 'meeting notes', })
🚫

Omitting fileName (or passing an empty string) rejects with INVALID_OPTIONS.

Sticker

The source (image or animated webp/gif/video) is converted to a WhatsApp sticker automatically.

await client.send(to).sticker('./logo.png')
OptionTypeDefaultDescription
animatedbooleanfalseMark the sticker as animated. Use for animated/looping sources.

Location

await client.send(to).location(-6.2, 106.816666, { name: 'Jakarta', address: 'Indonesia' })

The first two arguments are latitude then longitude.

Argument / OptionTypeDefaultDescription
lat (arg 1)numberrequiredLatitude, must be within -90..90.
lon (arg 2)numberrequiredLongitude, must be within -180..180.
namestringPlace name shown on the location card.
addressstringStreet address shown beneath the name.
🚫

Out-of-range coordinates reject with INVALID_OPTIONS.

Contact (vCard)

Send a contact card. Pass a raw vCard string — it must start with BEGIN:VCARD.

const vcard = [ 'BEGIN:VCARD', 'VERSION:3.0', 'FN:Zaileys Bot', 'TEL;type=CELL;type=VOICE;waid=6281234567890:+62 812-3456-7890', 'END:VCARD', ].join('\n') await client.send(to).contact(vcard)

contact() takes the vCard string directly (no options object). A string that does not begin with BEGIN:VCARD rejects with INVALID_OPTIONS.

Poll

await client.send(to).poll('Pick a colour', ['Red', 'Green', 'Blue'])

Signature: poll(question, options, opts?).

Argument / OptionTypeDefaultDescription
question (arg 1)stringrequiredPoll title. Must be non-empty.
options (arg 2)string[]required2–12 unique, non-empty choices.
multipleChoicebooleanfalseAllow selecting more than one option. When false, voters pick exactly one.
await client.send(to).poll('Toppings?', ['Cheese', 'Pepperoni', 'Mushroom'], { multipleChoice: true, })
🚫

Fewer than 2 or more than 12 options, empty strings, or duplicate options each reject with INVALID_OPTIONS (an empty question rejects with EMPTY_CONTENT).

Album

Send multiple images and/or videos as a single grouped album. Internally zaileys sends one parent message followed by each child, and resolves to the parent WAMessageKey.

await client.send(to).album([ { type: 'image', src: './a.jpg', caption: 'first' }, { type: 'video', src: './b.mp4' }, { type: 'image', src: imageBuffer }, ])

Each item is an AlbumItem:

FieldTypeDefaultDescription
type'image' | 'video'requiredMedia kind for this item.
srcMediaSourcerequiredURL, path, URL, or Buffer.
captionstringPer-item caption.
🚫

An album requires 2–30 items. Anything outside that range, or an item with a type other than image/video, rejects with INVALID_OPTIONS.


Modifiers (chaining)

Modifiers are orthogonal and can be chained on top of any content method, in any order, before the await.

await client .send(to) .text('Heads up, team!') .reply(quotedKeyOrMessage) // quote a message .mentions(['6281234567890@s.whatsapp.net']) // tag specific JIDs .disappearing(86400) // ephemeral, in seconds

.reply(quoted)

Quote an earlier message. Accepts either a full WAMessage (best — shows the quoted body) or a bare WAMessageKey. Passing null/undefined rejects with INVALID_OPTIONS.

const key = await client.send(to).text('Original') await client.send(to).text('A reply').reply(key) // inside a handler, you typically already have the full message: client.on('message', async (msg) => { await client.send(msg.chat.id).text('got it').reply(msg) })

.mentions(jids)

Tag specific participants. Each entry must be a JID-shaped string containing @. Repeated calls merge and de-duplicate; an empty array rejects with INVALID_OPTIONS.

await client .send(groupJid) .text('@alice @bob standup in 5') .mentions(['6281111111111@s.whatsapp.net', '6282222222222@s.whatsapp.net'])

.mentions() only sets the mention metadata. To make the names render as tappable links in the bubble, also include the matching @number text in your message body.

.mentionAll()

Tag every member of a group (no arguments).

await client.send(groupJid).text('Important: read this').mentionAll()

.disappearing(seconds)

Send as an ephemeral/disappearing message. Takes a positive integer number of seconds (e.g. 86400 = 24h, 604800 = 7 days). Non-positive or non-integer values reject with INVALID_OPTIONS.

await client.send(to).text('self-destructs in a day').disappearing(86400)

.to(recipient)

Reassign the recipient on an init-state builder before adding content (rarely needed since client.send(to) already sets it).

await client.send('placeholder').to('6281234567890@s.whatsapp.net').text('redirected')

The return value

Every successful send resolves to a WAMessageKey:

import type { WAMessageKey } from 'zaileys' const key: WAMessageKey = await client.send(to).text('keep this key') // key.id, key.remoteJid, key.fromMe … await client.edit(key).text('edited text') await client.delete(key, { forEveryone: true }) await client.forward(key, anotherJid)

For albums, the returned key is the parent message’s key. See Client for edit, delete, forward, and react.

Error handling

The builder throws typed ZaileysBuilderErrors. Common codes:

CodeWhen
EMPTY_CONTENTAwaited a builder with no content method set (or an empty poll question).
INVALID_OPTIONSBad arguments — missing fileName, out-of-range coords, bad poll/album size, empty mentions, etc.
MEDIA_LOAD_FAILEDA media source failed to load or transcode.
USERNAME_NOT_FOUNDA bare number/username could not be resolved to a WhatsApp account.
SEND_FAILEDThe underlying socket rejected the send or returned no key.
try { await client.send(to).document(buf, { fileName: 'x.pdf' }) } catch (err) { console.error('send failed:', err) }

See Error Handling for the full taxonomy.

What’s next

This page covers the plain content types. For richer experiences:

  • Interactive Messages.buttons(), .list(), .carousel(), .template().
  • Rich Responses.text(..., { rich: true }) AIRich cards (markdown, LaTeX).
  • Media Processing — converting, resizing, and inspecting media before sending.
  • Clientedit, delete, forward, react, and broadcast helpers.
Last updated on