Skip to Content
Message Payload

Message Payload (MessageContext)

Every inbound message handler receives a single MessageContext object. It is decoded by zaileys from the raw Baileys WAMessage into a flat, predictable shape: IDs are normalized, LIDs are resolved to phone numbers where possible, and the heavy parts (group name, replied message, media bytes) are exposed as lazy functions so nothing is fetched until you ask for it.

client.on('message', (ctx) => { console.log(ctx.senderId, ctx.text) if (ctx.isGroup) ctx.reply('hi from a group') })

The same payload shape is delivered to all of these events:

message · text · image · video · audio · document · sticker · mention · mention-all

mention / mention-all extend MessageContext with a couple of extra fields (the mentioned jids / group members). Everything documented here still applies.


Where it comes from

The raw Baileys message is run through the zaileys decode pipeline, which:

  1. Unwraps ephemeral / view-once / caption wrappers to find the real content.
  2. Normalizes jids and maps LID → phone number (with a short timeout) so senderId, roomId, mentions are stable PNs.
  3. Derives flags from protocolMessage, pinInChatMessage, contextInfo, etc.
  4. Defers the heavy work (group metadata, media download, quoted decode) behind functions you call on demand.

So plain fields are cheap to read; the function fields do real work (network/async) only when invoked.


Identity & IDs

FieldTypeSourceUse it for
uniqueIdstringFNV-1a hash of remoteJid │ messageId │ fromMeA fingerprint per message. Dedup, idempotency keys, logging. Changes if the message id / sender / direction changes.
staticIdstringFNV-1a hash of roomId │ senderIdStable id for a sender-in-a-room pair. Group per-user state, rate-limit buckets. Same across all that user’s messages in that room.
channelIdstringYour client config (empty string if unset)Tag messages with your own app/tenant channel. Multi-bot routing.
chatIdstringmessage.key.idThe WhatsApp message id (NOT a jid). Use to correlate with edits/reactions/receipts. ⚠️ Never pass this to client.send() as a target — use roomId/senderId.

Routing — who & where

FieldTypeSourceUse it for
roomIdstring | nullGroup → normalized group jid. 1-on-1 → the other party’s phone number (recipient if you sent it, sender if received).The reply target. Always points at the conversation, regardless of direction.
receiverIdstringConfig receiver / your own jid, normalized to PNThe intended recipient (the account that received the message).
senderIdstringkey.participant/remoteJid, mapped LID → PNWho sent it, as a phone-number jid (...@s.whatsapp.net). Primary identity for auth/owner checks.
senderLidstring | nullkey.participantAlt / remoteJidAlt ending in @lidThe sender’s LID, when WhatsApp exposes one. null if not available.
senderNamestring | nullmessage.pushNameDisplay/push name. Can be null or spoofed — don’t use for authz.
senderUsernamestring | nullkey.participantUsername / remoteJidUsernameThe sender’s WhatsApp username (the @handle), without the @. null unless WhatsApp sends one — see the note below.
senderDeviceSenderDeviceParsed from the sender’s device jidWhich client app sent it: android · ios · web · desktop · unknown.
addressingMode'pn' | 'lid'key.addressingModeWhether WhatsApp addressed this message by phone number or by LID. Useful when debugging identity resolution.
isFromMebooleankey.fromMe === truetrue if your own account sent it (echo of your sends, other-device activity).
isGroupbooleanremoteJid is a group jidBranch group vs DM logic.
isNewsletterbooleanremoteJid ends @newsletterMessage came from a channel/newsletter.
isBroadcastbooleanremoteJid ends @broadcastBroadcast-list message.
isStorybooleanremoteJid === 'status@broadcast'Status/story update.

Content

FieldTypeSourceUse it for
chatTypeChatTypeContent inspectionWhat kind of message this is (see values). Route by type.
textstringbody / caption / poll name / location label, after unwrappingThe textual content. Empty string if none. Caption text for media is mirrored here too.
timestampnumbermessageTimestamp, converted seconds → millisecondsUnix ms epoch. Feed straight into new Date(ctx.timestamp). 0 if invalid.
mentionsstring[]contextInfo.mentionedJid, mapped to PNJids tagged in the message.
mentionedGroupsstring[]contextInfo.groupMentionsGroups tagged in the message (community @group mentions).
linksstring[]Regex over textURLs found in the text (trailing punctuation trimmed).
forwardCountnumbercontextInfo.forwardingScoreHow many hops the message has been forwarded. 0 = not forwarded, >= 5 = WhatsApp’s “forwarded many times”.
ephemeralDurationnumber | nullcontextInfo.expirationThe chat’s disappearing timer in seconds, or null when messages are kept.
adAdAttribution | undefinedcontextInfo.externalAdReplyPresent only on the first message of a chat opened from a Meta ad. See ad.
businessBusinessInfo | undefinedverifiedBizNamePresent only when the sender is a verified WhatsApp Business account.

Flags (is…)

All computed from the message except isSpam (reserved, always false for now). A flag being false just means that state isn’t present on this message.

Fieldtrue when
isViewOnceview-once wrapper present, or media marked viewOnce.
isEphemeralephemeral wrapper present, or contextInfo.expiration > 0 (disappearing messages).
isForwardedmarked forwarded, or forwardingScore > 0.
isOldthe message is backlog — it arrived while your bot was disconnected and WhatsApp replayed it on reconnect. Guard on this so you don’t answer hours-old messages.
isQuestiontrimmed text ends with ?.
isPrefixtext starts with one of your configured command prefixes.
isTagMeyour jid is in mentions (you were @-tagged).
isEditedmessage is an edit (editedMessage / protocol edit).
isDeletedmessage is a revoke/delete.
isPinnedpin-for-all action.
isUnPinnedunpin-for-all action.
isBotcarries bot metadata (messageContextInfo.botMetadata).
isSpamreserved — always false.
isHideTagshidden mentions: mentions exist but the text has no visible @number.
isStatusMentiona status-mention message.
isGroupStatusMentiona group-status-mention message (someone mentioned this group in their status).
isGroupStatusthe message is a group status post. chatType still reflects the inner content (text, image, …).

The is* flags reflect the state of this specific message. In your example most are false simply because it’s a normal forwarded image — only isForwarded and isGroup are true.


Methods (lazy / on-demand)

These are functions, not values, so zaileys doesn’t fetch metadata or download bytes unless you call them. That’s why they print as [Function] if you console.log the whole object.

MethodReturnsWhat it does
roomName()Promise<string | null>Group subject (fetches group metadata) for groups, or the other party’s name for DMs.
receiverName()Promise<string | null>Display name of the recipient account.
replied()Promise<MessageContext | null>Decodes the quoted/replied message into a full MessageContext, or null if this isn’t a reply.
message()WAMessageThe original raw Baileys message (escape hatch for anything zaileys didn’t surface). Synchronous.
reply(content, opts?)Promise<WAMessageKey>Send a text reply into this conversation (quotes the message).
react(emoji)Promise<WAMessageKey>React to this message with an emoji.
client.on('message', async (ctx) => { const room = await ctx.roomName() const quoted = await ctx.replied() if (quoted) await ctx.reply(`you replied to: ${quoted.text}`) await ctx.react('👍') })

citation

ctx.citation.authors(): Promise<boolean> ctx.citation.banned(): Promise<boolean>

Predicates evaluated against your client’s citation config for the current sender:

  • authors() → is the sender in your configured authors allow-list (array of jids or a predicate)?
  • banned() → is the sender in your banned list?

Both resolve false when no citation config is set. Handy for owner/allow/deny gating without writing the jid check yourself.


media

Present only for messages that carry media or structured content. The shape depends on type.

Downloadable attachments (image · video · audio · document · sticker)

FieldTypeNotes
type'image' | 'video' | 'audio' | 'document' | 'sticker'
mimetypestring | nulle.g. image/jpeg.
captionstring | nullSame text as top-level text for captioned media.
fileNamestring | nullOriginal name (documents).
fileSizenumber | nullBytes, when WhatsApp provides it.
pttbooleantrue for a voice note (push-to-talk audio).
durationnumber | nullPlayback length in seconds (audio and video).
width / heightnumber | nullPixel dimensions (image, video, sticker).
pagesnumber | nullPage count (documents).
isAnimatedbooleantrue for an animated sticker or a GIF-playback video.
thumbnailBuffer | nullInline JPEG preview when WhatsApp sent one.
buffer()Promise<Buffer>Downloads the full media into memory.
stream()Promise<Readable>Streams the media (for large files).
client.on('image', async (ctx) => { if (ctx.media?.type === 'image') { const buf = await ctx.media.buffer() // save / process buf } })

The metadata fields arrive with the message, before any download. Use them to decide whether a download is worth it at all:

client.on('audio', async (ctx) => { if (ctx.media?.type !== 'audio') return if ((ctx.media.duration ?? 0) > 300) { return ctx.reply('Voice note terlalu panjang, maksimal 5 menit.') } const buf = await ctx.media.buffer() // only now do we pay for the download })

thumbnail lets you show a preview without downloading the original at all.

Structured media (no download)

Other chatTypes surface a typed media describing the content (no buffer()/stream()):

poll · contact · location / live-location · album · group-invite · product · order · payment · link (preview) · event · buttons · list · interactive · template

Each carries the fields relevant to it — e.g. location has latitude/longitude/name, poll has name/options/selectableCount, payment has kind/amount/currency, group-invite has groupId/inviteCode/expiresAt.


ad — where a lead came from

When someone taps a Click-to-WhatsApp ad on Facebook or Instagram, WhatsApp attaches the ad’s attribution to the first message of that chat. Later messages in the same chat do not carry it.

FieldTypeNotes
clickIdstring?The ctwaClid click id — join this against Meta Ads reporting.
sourceIdstring?The ad’s id.
sourceUrlstring?Landing URL configured on the ad.
sourceAppstring?Which surface the ad ran on, e.g. facebook, instagram.
sourceTypestring?Ad format as WhatsApp reports it.
title / bodystring?The ad’s headline and text.
thumbnailUrlstring?Ad creative preview.
refstring?Campaign reference you set on the ad.
client.on('message', async (ctx) => { if (!ctx.ad) return await saveLead({ from: ctx.senderId, campaign: ctx.ad.ref, clickId: ctx.ad.clickId }) await ctx.reply(`Halo! Kamu datang dari iklan "${ctx.ad.title}". Ada yang bisa dibantu?`) })

This is built from WhatsApp’s protocol definition but has not been verified against a live ad click — we have no Click-to-WhatsApp campaign to test with. The field names are correct; report an issue if a real ad message shapes differently.

business — verified business senders

FieldTypeNotes
verifiedNamestringThe WhatsApp-verified business name.

Present only when WhatsApp itself verified the sender. Unlike senderName (a push name anyone can set to anything) this cannot be spoofed, so prefer it when you display or log who you are talking to.


chatType values

text · image · video · audio · document · sticker poll · contact · location · live-location · event · album group-invite · product · order · payment buttons · list · interactive · template unknown

Media types are detected first, then structured message types; anything unrecognized is unknown.


senderDevice values

Parsed from the sender’s device jid:

ValueDevice id
androidnone / 0
ios2
web3
desktop4
unknownanything else

Config that shapes the payload

A few values come from your Client config rather than the raw message:

ConfigAffects
prefixesisPrefix
citation (authors / banned)ctx.citation.*
channelIdchannelId
receiverIdreceiverId
autoMention / self jidisTagMe, senderId resolution

Gotchas

  • chatId is a message id, not a jid. Reply with roomId / senderId, never chatId.
  • timestamp is milliseconds. Already converted from WhatsApp’s seconds — don’t multiply by 1000.
  • senderId is a phone-number jid (@s.whatsapp.net), resolved from LID when possible; senderLid holds the @lid form when present.
  • Methods are lazy & mostly async. roomName(), replied(), media.buffer(), citation.* do real work — await them; they show as [Function] when you log the object.
  • roomId follows the conversation, not the direction — safe as a reply target for both inbound and your own (isFromMe) messages.
  • isSpam is reserved and always false today. Use forwardCount >= 5 for chain-message detection.
  • Answer only live messages. After a reconnect WhatsApp replays everything you missed. Guard with if (ctx.isOld) return unless you deliberately want to process the backlog.
  • Disappearing timers are inherited automatically. zaileys learns each chat’s timer from its inbound messages, so ctx.reply() and every client.send(...) into that chat — text, media, stickers, albums — carry it without you asking. Pass .disappearing(seconds) only to override it. Messages sent without the timer are the ones WhatsApp flags “this message won’t disappear”.
  • senderUsername is usually null — WhatsApp sends the phone number or the username, never both. A username is the fallback handle for a sender whose number you cannot see, so it stays null for anyone whose phone number your account already knows (which is most people). It arrives mainly on group messages from LID-only senders. Never use it for authorization — senderId is the field for that. To ask for one explicitly, see client.getUsername(jid).
Last updated on