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: Vec<String>,
) -> Result<(), anyhow::Error>
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>
required
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
Vec<String>
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
    vec!["MESSAGE_ID_123".to_string()]
).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
    vec!["MESSAGE_ID_456".to_string()]
).await?;

Example: Mark Multiple Messages as Read

let message_ids = vec![
    "MSG_1".to_string(),
    "MSG_2".to_string(),
    "MSG_3".to_string(),
];

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.

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 mut builder = NodeBuilder::new("receipt")
    .attr("id", &info.id)
    .attr("to", &info.source.chat);

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();
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,
}
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
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 media messages (audio, video), you can send played receipts to indicate the media was played:
The library does not currently expose a public API for sending played receipts. This feature may be added in a future version.
Played receipts follow the same pattern as read receipts but use type="played":
<receipt id="MESSAGE_ID" to="CHAT_JID" type="played" participant="SENDER_JID" />
You can listen for incoming played receipts via the Event::Receipt event with ReceiptType::Played.

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<String> = 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
        vec![message_info.id.clone()]
    ).await?;
} else {
    client.mark_as_read(
        &message_info.source.chat,
        None, // No sender for DMs
        vec![message_info.id.clone()]
    ).await?;
}