Skip to content

fix(baileys): expose delivery receipts for group messages - #2674

Open
gersonkm wants to merge 1 commit into
evolution-foundation:developfrom
gersonkm:fix/expose-group-delivery-receipts
Open

fix(baileys): expose delivery receipts for group messages#2674
gersonkm wants to merge 1 commit into
evolution-foundation:developfrom
gersonkm:fix/expose-group-delivery-receipts

Conversation

@gersonkm

@gersonkm gersonkm commented Aug 5, 2026

Copy link
Copy Markdown

Problem

Delivery confirmation for group messages never reaches the webhook.

Individual chats confirm normally (SERVER_ACKDELIVERY_ACKREAD), but group messages produce no event at all — even though WhatsApp does send the receipt and Baileys does receive it, one per participant.

Root cause

Baileys routes receipts for group messages to message-receipt.update instead of messages.update (handleReceipt in messages-recv.ts):

if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
    const updateKey = status === DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp';
    ev.emit('message-receipt.update', ...)
}

Note that delivery arrives as receiptTimestamp, while read arrives as readTimestamp. The handler for that event consumes only readTimestamp and never calls sendDataWebhook, so the delivery signal is silently dropped.

Evidence

Measured on 2.3.7 with LOG_BAILEYS=debug. A single message sent to a 3-participant group produced four receipt stanzas (identifiers redacted):

{"recv":{"tag":"receipt","attrs":{"from":"<group>@g.us","id":"<msg-id>","participant":"<user-a>:11@lid"}}}
{"recv":{"tag":"receipt","attrs":{"from":"<group>@g.us","id":"<msg-id>","participant":"<user-b>:6@lid"}}}
{"recv":{"tag":"receipt","attrs":{"from":"<group>@g.us","id":"<msg-id>","participant":"<user-b>@lid"}}}
{"recv":{"tag":"receipt","attrs":{"from":"<group>@g.us","type":"read","id":"<msg-id>","participant":"<user-a>:11@lid"}}}

Webhook calls in the same window: zero.

With this patch applied, the same test produces four MESSAGES_UPDATE events with participant populated and status DELIVERY_ACK / READ.

Approach

Emits MESSAGES_UPDATE using the same flat payload as the individual-chat branch, so that:

  • no new event type is needed in the Events enum or in the per-integration schemas (webhook, RabbitMQ, NATS, SQS, Kafka, Pusher);
  • existing consumers keep working — participant is already part of the status schema;
  • the existing readTimestamp handling is left untouched.

Notes

  • Running in production since 2026-08-05, on top of 2.3.7.
  • npm run build and npm run lint:check pass.
  • No tests added: the repository has no coverage around this event path, and reproducing it requires a live WhatsApp session with a group.

@sourcery-ai

sourcery-ai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Routes Baileys group-message delivery/read receipts into the existing MESSAGES_UPDATE webhook path by emitting a flat status payload for each receipt and inferring DELIVERY_ACK vs READ from the receipt timestamps, fixing missing delivery confirmations for group chats without introducing new event types.

Sequence diagram for routing Baileys group receipts to MESSAGES_UPDATE

sequenceDiagram
    participant WhatsApp
    participant Baileys as BaileysCore
    participant Startup as BaileysStartupService
    participant Webhook as WebhookEndpoint

    WhatsApp->>Baileys: handleReceipt
    Baileys->>Baileys: ev.emit(message-receipt.update)
    Baileys->>Startup: message-receipt.update(payload)
    loop for each event
        Startup->>Startup: sendDataWebhook(Events.MESSAGES_UPDATE)
        Startup->>Webhook: sendDataWebhook(Events.MESSAGES_UPDATE)
    end
Loading

File-Level Changes

Change Details Files
Emit MESSAGES_UPDATE webhook events for group message receipts so delivery/read confirmations reach integrators.
  • Hook into Baileys message-receipt.update payload loop to process each receipt event
  • Call sendDataWebhook with a flat payload mirroring the individual-chat messages.update branch, including keyId, remoteJid, fromMe, participant, status, and instanceId
  • Derive status as DELIVERY_ACK when receiptTimestamp is numeric, otherwise READ, aligning with Baileys receipt semantics while preserving existing readTimestamp handling
  • Preserve existing aggregation of readTimestamp into remotesJidMap for backward-compatible behavior
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The status derivation based solely on typeof event.receipt.receiptTimestamp === 'number' may misclassify read receipts if both receiptTimestamp and readTimestamp are present; consider explicitly checking for readTimestamp (or Baileys’ status field) to distinguish DELIVERY vs READ more robustly.
  • You’re emitting MESSAGES_UPDATE for every message-receipt.update event before the existing readTimestamp handling; double-check whether this causes duplicated updates for reads and, if so, gate the emission to avoid sending two different updates for the same read event.
  • The in-code explanation comment is quite long and specific; consider tightening it and/or linking to an issue or reference so future readers get context without carrying too much narrative in the implementation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The status derivation based solely on `typeof event.receipt.receiptTimestamp === 'number'` may misclassify read receipts if both `receiptTimestamp` and `readTimestamp` are present; consider explicitly checking for `readTimestamp` (or Baileys’ `status` field) to distinguish DELIVERY vs READ more robustly.
- You’re emitting `MESSAGES_UPDATE` for every `message-receipt.update` event before the existing `readTimestamp` handling; double-check whether this causes duplicated updates for reads and, if so, gate the emission to avoid sending two different updates for the same read event.
- The in-code explanation comment is quite long and specific; consider tightening it and/or linking to an issue or reference so future readers get context without carrying too much narrative in the implementation.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Baileys routes receipts for group messages to `message-receipt.update`
instead of `messages.update` (see `handleReceipt` in messages-recv.ts).
In that branch, delivery arrives as `receiptTimestamp` while read
arrives as `readTimestamp` — but only `readTimestamp` was being
consumed, and `sendDataWebhook` was never called.

The practical effect is that delivery confirmation for group messages
never reaches the webhook. Individual chats confirm normally
(SERVER_ACK, DELIVERY_ACK, READ); groups produce no event at all, even
though WhatsApp does send the receipt and Baileys does receive it — one
per participant.

Verified on 2.3.7 with `LOG_BAILEYS=debug`: a single group message
produced four `tag: "receipt"` stanzas, and zero webhook calls.

This emits MESSAGES_UPDATE using the same flat payload as the
individual-chat branch, so no new event type is needed and existing
consumers keep working — `participant` is already part of the status
schema. The existing `readTimestamp` handling is untouched.
@gersonkm
gersonkm force-pushed the fix/expose-group-delivery-receipts branch from c55a529 to 164421f Compare August 5, 2026 04:55
@gersonkm

gersonkm commented Aug 5, 2026

Copy link
Copy Markdown
Author

Thanks for the review. I looked into all three points — one is addressed, two don't apply, and I think it's worth showing why.

1. receiptTimestamp vs readTimestamp — they can't coexist

Baileys builds the receipt object with a computed key, so exactly one of the two fields exists per event (Socket/messages-recv.ts):

const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp';
ev.emit('message-receipt.update', ids.map(id => ({
    key: { ...key, id },
    receipt: {
        userJid: jidNormalizedUser(attrs.participant),
        [updateKey]: +attrs.t
    }
})));

The check in this PR is the mirror image of how Baileys populates the object, so a read receipt can't be misclassified as delivery — receiptTimestamp is simply absent on it.

2. Duplicate updates for reads

updateMessagesReadedByTimestamp doesn't emit anything — it runs a $executeRaw UPDATE and returns the affected row count. There is no sendDataWebhook call in that path, so a read produces exactly one webhook event (the one added here) plus the same database update as before.

3. Comment length — fixed

Fair point, and applied: trimmed from 9 lines to 4, keeping only what isn't obvious from the code (that group receipts land on a different event, and which field carries delivery). The longer narrative lives in the PR description, which is where future readers will look for it.

Let me know if you'd prefer the status derivation to read Baileys' status field explicitly instead — I kept it timestamp-based to match how the surrounding code already works, but I'm happy to change it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant