> ## Documentation Index
> Fetch the complete documentation index at: https://personal-9eca1d6c-claude-eager-cerf-0xyrbp.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# receipt

> Send read receipts, delivery receipts, and played receipts

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

```rust theme={null}
pub async fn mark_as_read(
    &self,
    chat: &Jid,
    sender: Option<&Jid>,
    message_ids: &[&str],
) -> Result<(), anyhow::Error>
```

<Note>
  `message_ids` is a borrowed slice `&[&str]` to avoid per-call allocations — pass `&["ID"]` or `&["ID_1", "ID_2"]`.
</Note>

<ParamField path="chat" type="&Jid" required>
  Chat JID where the messages were received. Can be:

  * Direct message: `15551234567@s.whatsapp.net`
  * Group: `120363040237990503@g.us`
</ParamField>

<ParamField path="sender" type="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)
</ParamField>

<ParamField path="message_ids" type="&[&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.
</ParamField>

### Example: mark DM as read

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
let message_ids = ["MSG_1", "MSG_2", "MSG_3"];

client.mark_as_read(&chat_jid, None, &message_ids).await?;
```

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

<Note>
  `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.
</Note>

***

## 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`](#mark_as_read) instead.

```rust theme={null}
pub async fn mark_as_played(
    &self,
    chat: &Jid,
    sender: Option<&Jid>,
    message_ids: &[&str],
) -> Result<(), anyhow::Error>
```

<Note>
  `message_ids` is a borrowed slice `&[&str]`, matching [`mark_as_read`](#mark_as_read).
</Note>

<ParamField path="chat" type="&Jid" required>
  Chat JID where the media message was received. Can be a direct message, group, broadcast list, or newsletter JID.
</ParamField>

<ParamField path="sender" type="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.
</ParamField>

<ParamField path="message_ids" type="&[&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.
</ParamField>

### Example: mark a DM voice note as played

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
client.mark_as_played(
    &chat_jid,
    None,
    &["MSG_1", "MSG_2", "MSG_3"],
).await?;
```

<Note>
  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`.
</Note>

***

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

```rust theme={null}
pub(crate) async fn send_delivery_receipt(
    &self,
    info: &Arc<MessageInfo>
)
```

<ParamField path="info" type="&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
</ParamField>

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

<Note>
  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()`.
</Note>

### Wire format

Internally, delivery receipts pass JID references directly to the `.attr()` method, avoiding allocations on the hot path:

```rust theme={null}
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();
```

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

Read receipts (`mark_as_read`) batch multiple message IDs using a `<list>` child node with `<item>` elements:

```rust theme={null}
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:

```rust theme={null}
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
}
```

<ParamField path="Delivered" type="variant">
  Delivery receipt (type=`""`). Confirms message was delivered to the recipient's device. Sent automatically by the library.
</ParamField>

<ParamField path="Read" type="variant">
  Read receipt (type=`"read"`). Confirms message was read by the recipient. Sent manually via `mark_as_read()`.
</ParamField>

<ParamField path="ReadSelf" type="variant">
  Read receipt from your own device (type=`"read-self"`). Received when you read a message on another device.
</ParamField>

<ParamField path="Played" type="variant">
  Played receipt (type=`"played"`). Confirms media (audio/video) was played by the recipient.
</ParamField>

<ParamField path="PlayedSelf" type="variant">
  Played receipt from your own device (type=`"played-self"`). Received when you play media on another device.
</ParamField>

<ParamField path="Retry" type="variant">
  Retry receipt (type=`"retry"`). Recipient failed to decrypt the message and is requesting a retry. Automatically handled by the library.
</ParamField>

<ParamField path="EncRekeyRetry" type="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.
</ParamField>

<ParamField path="Sender" type="variant">
  Sender receipt (type=`"sender"`). Acknowledges message was sent.
</ParamField>

<ParamField path="ServerError" type="variant">
  Server error receipt (type=`"server-error"`). Message delivery failed on server.
</ParamField>

***

## Receipt Events

You can listen for receipt events to track message delivery and read status using the Bot event handler:

```rust theme={null}
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

```rust theme={null}
pub struct Receipt {
    pub source: MessageSource,
    pub message_ids: Vec<String>,
    pub timestamp: DateTime<Utc>,
    pub r#type: ReceiptType,
    pub offline: bool,
}
```

<ParamField path="source" type="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
</ParamField>

<ParamField path="offline" type="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.
</ParamField>

<ParamField path="message_ids" type="Vec<String>">
  List of message IDs this receipt applies to. Usually contains a single ID, but can have multiple.
</ParamField>

<ParamField path="timestamp" type="DateTime<Utc>">
  When the receipt was received (local time)
</ParamField>

<ParamField path="r#type" type="ReceiptType">
  Type of receipt (Delivered, Read, Played, etc.)
</ParamField>

***

## Message tracking example

Track message delivery and read status:

```rust theme={null}
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`](#mark_as_played) once the user has played the media. Newsletters send `played-self`; everything else sends `played`. The wire shape mirrors read receipts:

```xml theme={null}
<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

<Tip>
  Respect user privacy settings. If you're building a client, consider adding a setting to disable read receipts.
</Tip>

```rust theme={null}
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:

```rust theme={null}
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:

```rust theme={null}
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?;
}
```
