Skip to main content

mark_as_read

Send read receipts for one or more messages. Read receipts inform the sender that you’ve read their message(s). For group messages, you must pass the original sender’s JID as the sender parameter.
pub async fn mark_as_read(
    &self,
    chat: &Jid,
    sender: Option<&Jid>,
    message_ids: &[&str],
) -> Result<(), anyhow::Error>
message_ids is a borrowed slice &[&str] to avoid per-call allocations — pass &["ID"] or &["ID_1", "ID_2"].
chat
&Jid
required
Chat JID where the messages were received. Can be:
  • Direct message: 15551234567@s.whatsapp.net
  • Group: 120363040237990503@g.us
sender
Option<&Jid>
Message sender JID. Required for group messages, None for direct messages.
  • For DMs: Pass None
  • For groups: Pass the JID of the user who sent the message(s)
message_ids
&[&str]
required
List of message IDs to mark as read. Can be a single ID or multiple IDs.If empty, this function returns immediately without sending anything.

Example: mark DM as read

use wacore_binary::jid::Jid;

let chat_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

client.mark_as_read(
    &chat_jid,
    None, // No sender for DMs
    &["MESSAGE_ID_123"],
).await?;

Example: mark group message as read

let group_jid: Jid = "120363040237990503@g.us".parse()?;
let sender_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

client.mark_as_read(
    &group_jid,
    Some(&sender_jid), // Must specify sender in groups
    &["MESSAGE_ID_456"],
).await?;

Example: mark multiple messages as read

let message_ids = ["MSG_1", "MSG_2", "MSG_3"];

client.mark_as_read(&chat_jid, None, &message_ids).await?;
Read receipts are not sent automatically by the library. You must explicitly call mark_as_read() when you want to notify the sender that messages have been read.
mark_as_read adapts the wire shape to the chat, matching WhatsApp Web. A newsletter read is sent as read-self. A status@broadcast read carries context="status". When the status author is a LID, it also adds peer_participant_pn (the resolved LID→PN). The same call handles all three cases — you don’t pass anything extra.

mark_as_played

Send played receipts for one or more voice notes or video notes. Played receipts tell the sender that you’ve listened to their voice note or watched their video note — they’re the equivalent of “read” for playable media. Call this only after the user has actually played the media; if you just want to acknowledge that the message was opened, use mark_as_read instead.
pub async fn mark_as_played(
    &self,
    chat: &Jid,
    sender: Option<&Jid>,
    message_ids: &[&str],
) -> Result<(), anyhow::Error>
message_ids is a borrowed slice &[&str], matching mark_as_read.
chat
&Jid
required
Chat JID where the media message was received. Can be a direct message, group, broadcast list, or newsletter JID.
sender
Option<&Jid>
Original sender JID.
  • For DMs: pass None (the participant attribute is dropped on the wire, matching WhatsApp Web).
  • For groups, broadcast lists, and status broadcasts: pass the JID of the user who sent the media.
  • For newsletters: the receipt is sent as played-self; sender is ignored.
message_ids
&[&str]
required
Message IDs of the voice or video notes to mark as played. The first ID becomes the receipt’s id attribute; any additional IDs are batched into a <list><item/></list> child, the same shape as mark_as_read.If empty, this function returns immediately without sending anything.

Example: mark a DM voice note as played

use wacore_binary::jid::Jid;

let chat_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

client.mark_as_played(
    &chat_jid,
    None, // No participant in DMs
    &["VOICE_MSG_ID"],
).await?;

Example: mark a group voice note as played

let group_jid: Jid = "120363040237990503@g.us".parse()?;
let sender_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

client.mark_as_played(
    &group_jid,
    Some(&sender_jid), // Required in groups and broadcasts
    &["VOICE_MSG_ID"],
).await?;

Example: mark multiple voice notes as played

client.mark_as_played(
    &chat_jid,
    None,
    &["MSG_1", "MSG_2", "MSG_3"],
).await?;
Played receipts are not sent automatically — call mark_as_played() from your media player when the user finishes (or starts) playing the audio or video note. The library does not gate this call on the recipient’s read-receipts privacy setting, matching mark_as_read.

send_delivery_receipt (Internal)

Sends a delivery receipt to the sender of a message. This is an internal method called automatically by the library when messages are received. You typically don’t need to call this directly.
pub(crate) async fn send_delivery_receipt(
    &self,
    info: &Arc<MessageInfo>
)
info
&Arc<MessageInfo>
required
Message metadata containing:
  • id - Message ID
  • source.chat - Chat JID
  • source.sender - Sender JID
  • source.is_from_me - Whether this is your own message
  • source.is_group - Whether this is a group message

Behavior

Delivery receipts are automatically sent for all incoming messages except:
  • Your own messages (is_from_me = true)
  • Messages without an ID
  • Status broadcast messages (status@broadcast)
  • Newsletter messages
For group messages, the receipt includes a participant attribute identifying the sender.
Delivery receipts are sent automatically. Unlike other receipt types (e.g., type="read", type="played"), delivery receipts have no type attribute on the wire — delivery is the implicit default. The library omits the type attribute from ack responses to delivery receipts accordingly, since including an explicit type="delivery" would cause <stream:error> disconnections from the server. This is different from read receipts (type="read"), which you send manually with mark_as_read().

Wire format

Internally, delivery receipts pass JID references directly to the .attr() method, avoiding allocations on the hot path:
let is_status = info.source.chat.is_status_broadcast();
// For 1:1 DMs the `to` echoes the sender JID verbatim so the multi-device
// LID device byte (e.g. `…:7@lid`) survives. Group / status receipts stay
// addressed at the chat JID since those never carry a device.
let to = if info.source.is_group || is_status {
    &info.source.chat
} else {
    &info.source.sender
};

let mut builder = NodeBuilder::new("receipt")
    .attr("id", &info.id)
    .attr("to", to);

if info.category == MessageCategory::Peer {
    builder = builder.attr("type", "peer_msg");
}

if info.source.is_group {
    builder = builder.attr("participant", &info.source.sender);
}

let receipt_node = builder.build();
v0.6 split the to attribute by addressing case. Earlier versions always used info.source.chat, which strips the device byte (to_non_ad). For multi-device LID senders that arrived with from="USER:DEV@lid", the device-less receipt was rejected by the LID server: it replayed the stanza from the offline queue and eventually closed the stream with <stream:error><ack class="message"/></stream:error>. Matching whatsmeow’s buildBaseReceipt (which echoes node.Attrs["from"] verbatim) and WA Web’s sendDeliveryReceiptsAfterDecryption resolves the issue.
Read receipts (mark_as_read) batch multiple message IDs using a <list> child node with <item> elements:
let mut builder = NodeBuilder::new("receipt")
    .attr("to", chat)
    .attr("type", "read")
    .attr("id", &message_ids[0])
    .attr("t", &timestamp);

if let Some(sender) = sender {
    builder = builder.attr("participant", sender);
}

// Additional message IDs beyond the first
if message_ids.len() > 1 {
    let items: Vec<Node> = message_ids[1..]
        .iter()
        .map(|id| NodeBuilder::new("item").attr("id", id).build())
        .collect();
    builder = builder.children(vec![
        NodeBuilder::new("list").children(items).build()
    ]);
}

Receipt Types

WhatsApp supports multiple receipt types:
pub enum ReceiptType {
    Delivered,      // Message delivered to device
    Sender,         // Sender receipt
    Retry,          // Decryption retry request
    EncRekeyRetry,  // VoIP call encryption re-keying retry
    Read,           // Message read by recipient
    ReadSelf,       // Message read on another device
    Played,         // Media played by recipient
    PlayedSelf,     // Media played on another device
    ServerError,    // Server error
    Inactive,       // Inactive participant
    PeerMsg,        // Peer message
    HistorySync,    // History sync
    Other(String),  // Unknown receipt type
}
Delivered
variant
Delivery receipt (type=""). Confirms message was delivered to the recipient’s device. Sent automatically by the library.
Read
variant
Read receipt (type="read"). Confirms message was read by the recipient. Sent manually via mark_as_read().
ReadSelf
variant
Read receipt from your own device (type="read-self"). Received when you read a message on another device.
Played
variant
Played receipt (type="played"). Confirms media (audio/video) was played by the recipient.
PlayedSelf
variant
Played receipt from your own device (type="played-self"). Received when you play media on another device.
Retry
variant
Retry receipt (type="retry"). Recipient failed to decrypt the message and is requesting a retry. Automatically handled by the library.
EncRekeyRetry
variant
VoIP call encryption re-keying retry receipt (type="enc_rekey_retry"). Sent when a peer fails to decrypt VoIP call encryption data and needs the sender to re-key. Uses an <enc_rekey> child element (with call-creator, call-id, count attributes) instead of the standard <retry> child. Automatically handled by the library.
Sender
variant
Sender receipt (type="sender"). Acknowledges message was sent.
ServerError
variant
Server error receipt (type="server-error"). Message delivery failed on server.

Receipt Events

You can listen for receipt events to track message delivery and read status using the Bot event handler:
use wacore::types::events::Event;

let mut bot = Bot::builder()
    .with_backend(backend)
    .with_transport_factory(transport_factory)
    .with_http_client(http_client)
    .on_event(|event, client| async move {
        if let Event::Receipt(receipt) = &*event {
            println!("Receipt type: {:?}", receipt.r#type);
            println!("From: {}", receipt.source.sender);
            println!("Message IDs: {:?}", receipt.message_ids);
            
            match receipt.r#type {
                ReceiptType::Delivered => {
                    println!("Message delivered");
                }
                ReceiptType::Read => {
                    println!("Message read");
                }
                ReceiptType::Played => {
                    println!("Media played");
                }
                _ => {}
            }
        }
    })
    .build()
    .await?;

Receipt event structure

pub struct Receipt {
    pub source: MessageSource,
    pub message_ids: Vec<String>,
    pub timestamp: DateTime<Utc>,
    pub r#type: ReceiptType,
    pub offline: bool,
}
source
MessageSource
Source information:
  • chat - Chat JID where the receipt originated
  • sender - JID of the user who sent the receipt
  • is_group - Whether this is from a group
offline
bool
true when the receipt carried the offline attribute. That means it was drained from the server’s offline queue on reconnect rather than delivered live. Use it to tell a backlog of receipts received at login apart from real-time ones.
message_ids
Vec<String>
List of message IDs this receipt applies to. Usually contains a single ID, but can have multiple.
timestamp
DateTime<Utc>
When the receipt was received (local time)
r#type
ReceiptType
Type of receipt (Delivered, Read, Played, etc.)

Message tracking example

Track message delivery and read status:
use std::collections::HashMap;
use wacore::types::events::{Event, ReceiptType};

#[derive(Default)]
struct MessageTracker {
    delivered: HashMap<String, bool>,
    read: HashMap<String, bool>,
}

impl MessageTracker {
    fn track_receipt(&mut self, receipt: &Receipt) {
        for msg_id in &receipt.message_ids {
            match receipt.r#type {
                ReceiptType::Delivered => {
                    self.delivered.insert(msg_id.clone(), true);
                }
                ReceiptType::Read => {
                    self.read.insert(msg_id.clone(), true);
                }
                _ => {}
            }
        }
    }
    
    fn is_delivered(&self, msg_id: &str) -> bool {
        self.delivered.get(msg_id).copied().unwrap_or(false)
    }
    
    fn is_read(&self, msg_id: &str) -> bool {
        self.read.get(msg_id).copied().unwrap_or(false)
    }
}

let tracker = Arc::new(Mutex::new(MessageTracker::default()));

// Use within Bot event handler
let tracker_clone = tracker.clone();
let mut bot = Bot::builder()
    // ... configure backend, transport, http_client ...
    .on_event(move |event, _client| {
        let tracker = tracker_clone.clone();
        async move {
            if let Event::Receipt(receipt) = &*event {
                tracker.lock().await.track_receipt(receipt);
            }
        }
    })
    .build()
    .await?;

Played receipts (media)

For voice notes and video notes, send played receipts with mark_as_played once the user has played the media. Newsletters send played-self; everything else sends played. The wire shape mirrors read receipts:
<receipt id="MESSAGE_ID" to="CHAT_JID" type="played" participant="SENDER_JID" />
You can also listen for incoming played receipts via the Event::Receipt event with ReceiptType::Played or ReceiptType::PlayedSelf.

Best Practices

Read receipt privacy

Respect user privacy settings. If you’re building a client, consider adding a setting to disable read receipts.
struct Settings {
    send_read_receipts: bool,
}

if settings.send_read_receipts {
    client.mark_as_read(&chat_jid, sender, &message_ids).await?;
}

Batching multiple receipts

Send read receipts for multiple messages at once to reduce network overhead:
let mut pending_receipts: Vec<&str> = Vec::new();

// Collect message IDs
pending_receipts.push(msg_id_1);
pending_receipts.push(msg_id_2);
pending_receipts.push(msg_id_3);

// Send batch
if !pending_receipts.is_empty() {
    client.mark_as_read(&chat_jid, None, &pending_receipts).await?;
}

Group message receipts

Always include the sender JID for group messages:
if message_info.source.is_group {
    client.mark_as_read(
        &message_info.source.chat,
        Some(&message_info.source.sender), // Required for groups
        &[message_info.id.as_str()],
    ).await?;
} else {
    client.mark_as_read(
        &message_info.source.chat,
        None, // No sender for DMs
        &[message_info.id.as_str()],
    ).await?;
}