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.
senderDeviceSenderDeviceParsed from the sender’s device jidWhich client app sent it: android · ios · web · desktop · unknown.
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.
linksstring[]Regex over textURLs found in the text (trailing punctuation trimmed).

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.
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.

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).
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 } })

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.


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.
Last updated on