> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# June 5, 2026

> Typed MEX operations, reaction helpers, newsletter edit/revoke, and an E2E send-path guard for newsletter JIDs.

## Breaking changes

**Typed MEX operations**
[`MexRequest`](/api/mex#mexrequest) is now generic over its variables type (`MexRequest<V>`), and the request carries a [`MexDoc`](/api/mex#mexdoc) (operation name + persisted document ID) instead of a free-form `doc_id: &str`. Variables are serialized straight to the wire — there's no intermediate `serde_json::Value` step — so callers pass either a typed `Variables` struct from a generated operation module or any `serde::Serialize` value.

The generated modules live under [`wacore::iq::mex_operations`](/api/wacore) and expose `NAME`, `DOC_ID`, `OPERATION_KIND`, `Variables`, and `Response` per persisted query. Build a request with [`MexRequest::new`](/api/mex#new):

```rust theme={null}
use wacore::iq::mex_operations::join_newsletter;
use whatsapp_rust::features::mex::MexRequest;

let request = MexRequest::new(
    join_newsletter::NAME,
    join_newsletter::DOC_ID,
    join_newsletter::Variables {
        newsletter_id: Some(jid.to_string()),
    },
);

let response = client.mex().mutate(request).await?;
```

`Mex::query` and `Mex::mutate` are now generic over `V: Serialize`, and a new `MexError::InvalidJid` variant surfaces JID parsing failures from MEX payloads. See [MEX (GraphQL)](/api/mex) for the full reference.

<Note>
  If you previously built requests with `MexRequest { doc_id, variables }`, switch to `MexRequest::new(NAME, DOC_ID, variables)` using the generated module's constants — or, for ad-hoc operations, set `doc: MexDoc { name, id }` directly. Numeric `DOC_ID`s rotate with WhatsApp Web bundle releases; the stable `NAME` keeps diagnostics readable across versions.
</Note>

## New features

**`Client::send_reaction` helper**
Send a reaction to a DM, group, or `status@broadcast` message in one call. The helper builds the underlying `ReactionMessage` (including `sender_timestamp_ms`) and routes the stanza through the standard send path, so retry, fan-out, and phash handling all apply. Pass an empty string to remove a previous reaction. For groups and status, set `target_key.participant` to the original sender so the receipt can be attributed. See [`send_reaction`](/api/send#send_reaction).

```rust theme={null}
use waproto::whatsapp as wa;

let target_key = wa::MessageKey {
    remote_jid: Some(group_jid.to_string()),
    from_me: Some(false),
    id: Some(target_message_id.clone()),
    participant: Some(sender_jid.to_string()), // required for groups/status
};

client.send_reaction(&group_jid, target_key, "🎉").await?;
```

**`MessageContext::react` shortcut**
Inside an event handler, [`MessageContext::react`](/api/bot#react) reacts to the incoming message without manually rebuilding the message key — it fills in `chat`, message ID, and group/status `participant` for you.

```rust theme={null}
use whatsapp_rust::bot::MessageContext;

.on_event(|event, client| async move {
    if let Some(ctx) = MessageContext::from_event(&event, client) {
        let _ = ctx.react("👍").await;
    }
})
```

<Note>
  Newsletter (channel) reactions still go through [`client.newsletter().send_reaction()`](/api/newsletter#send_reaction) because they use a different plaintext stanza format.
</Note>

**Newsletter channel edit and revoke**
Channel messages now have first-class edit and revoke helpers on `client.newsletter()`. Both go out as plaintext `<message>` stanzas (no Signal encryption) and key off the message's wire `message_id` — not the `server_id` used for reactions. See [`edit_message`](/api/newsletter#edit_message) and [`revoke_message`](/api/newsletter#revoke_message).

```rust theme={null}
use waproto::whatsapp as wa;

let new_body = wa::Message {
    conversation: Some("Updated announcement".to_string()),
    ..Default::default()
};

client.newsletter()
    .edit_message(&newsletter_jid, message_id.clone(), new_body)
    .await?;

client.newsletter()
    .revoke_message(&newsletter_jid, message_id)
    .await?;
```

**Newsletter JIDs rejected on the E2E send path**
`send_message_impl`, `pin_message`, and `Client::edit_message` / `Client::revoke_message` now reject newsletter JIDs at the root of the encrypted send path. A mis-routed channel JID surfaces a clear error that names the mis-route instead of producing a malformed encrypted fan-out. Use `client.newsletter()` for any channel send, edit, or revoke.
