The Newsletter feature provides methods for managing WhatsApp newsletter channels, including creation, subscription management, reactions, and live updates. Newsletter operations use MEX (GraphQL) for metadata/management and IQ stanzas for message operations.
Newsletter message sending is handled by the unified client.send_message() method — see the Send API. Reactions remain on the Newsletter struct because they use a different stanza format.
Newsletter messages are plaintext — they are not encrypted with the Signal protocol.
Access
Access newsletter operations through the client:
let newsletter = client.newsletter();
Methods
list_subscribed
List all newsletters the user is subscribed to.
pub async fn list_subscribed(&self) -> Result<Vec<NewsletterMetadata>, NewsletterError>
Returns:
Vec<NewsletterMetadata> — List of subscribed newsletters
Example:
let newsletters = client.newsletter().list_subscribed().await?;
for nl in &newsletters {
println!("{}: {} ({} subscribers)", nl.jid, nl.name, nl.subscriber_count);
}
Fetch metadata for a newsletter by its JID.
pub async fn get_metadata(&self, jid: &Jid) -> Result<NewsletterMetadata, NewsletterError>
Parameters:
jid — Newsletter JID (server must be newsletter)
Returns:
NewsletterMetadata — Full newsletter metadata
Example:
let metadata = client.newsletter().get_metadata(&newsletter_jid).await?;
println!("Name: {}", metadata.name);
println!("Subscribers: {}", metadata.subscriber_count);
println!("Verified: {:?}", metadata.verification);
Fetch metadata for a newsletter by its invite code.
pub async fn get_metadata_by_invite(
&self,
invite_code: &str,
) -> Result<NewsletterMetadata, NewsletterError>
Parameters:
invite_code — Newsletter invite code string
Returns:
NewsletterMetadata — Full newsletter metadata
Example:
let metadata = client.newsletter()
.get_metadata_by_invite("ABC123")
.await?;
println!("Found: {} ({})", metadata.name, metadata.jid);
create
Create a new newsletter.
pub async fn create(
&self,
name: &str,
description: Option<&str>,
) -> Result<NewsletterMetadata, NewsletterError>
Parameters:
name — Newsletter name
description — Optional description
Returns:
NewsletterMetadata — Metadata of the newly created newsletter
Example:
let created = client.newsletter()
.create("My Channel", Some("A description"))
.await?;
println!("Created: {} ({})", created.name, created.jid);
join
Join (subscribe to) a newsletter.
pub async fn join(&self, jid: &Jid) -> Result<NewsletterMetadata, NewsletterError>
Parameters:
jid — Newsletter JID to join
Returns:
NewsletterMetadata — Metadata with the viewer’s role set to Subscriber
Example:
let joined = client.newsletter().join(&newsletter_jid).await?;
println!("Joined '{}' as {:?}", joined.name, joined.role);
leave
Leave (unsubscribe from) a newsletter.
pub async fn leave(&self, jid: &Jid) -> Result<(), NewsletterError>
Parameters:
jid — Newsletter JID to leave
Example:
client.newsletter().leave(&newsletter_jid).await?;
update
Update a newsletter’s name and/or description.
pub async fn update(
&self,
jid: &Jid,
name: Option<&str>,
description: Option<&str>,
) -> Result<NewsletterMetadata, NewsletterError>
Parameters:
jid — Newsletter JID
name — New name, or None to keep the current name
description — New description, or None to keep the current description
Returns:
NewsletterMetadata — Updated metadata
Example:
let updated = client.newsletter()
.update(&newsletter_jid, Some("New Name"), None)
.await?;
println!("Updated: {}", updated.name);
set_follower_mute
Mute or unmute a newsletter’s follower-activity notifications (WhatsApp Web’s MUTE_FOLLOWER_ACTIVITY). Sent via MEX as a user-setting update.
pub async fn set_follower_mute(&self, jid: &Jid, muted: bool) -> Result<(), NewsletterError>
Parameters:
jid — Newsletter JID
muted — true silences notifications, false re-enables them
Example:
// Mute
client.newsletter().set_follower_mute(&newsletter_jid, true).await?;
// Unmute
client.newsletter().set_follower_mute(&newsletter_jid, false).await?;
set_admin_mute
Mute or unmute a newsletter’s admin-activity notifications (WhatsApp Web’s MUTE_ADMIN_ACTIVITY). Only meaningful for owners/admins.
pub async fn set_admin_mute(&self, jid: &Jid, muted: bool) -> Result<(), NewsletterError>
Parameters:
jid — Newsletter JID
muted — true silences admin-activity notifications, false re-enables them
Example:
client.newsletter().set_admin_mute(&newsletter_jid, true).await?;
The mute state is sent as ON/OFF; the mute expiration is local database state and is never put on the wire, matching WhatsApp Web’s WAWebNewsletterUpdateUserSettingJob.
Sending messages
Newsletter message sending is handled by the unified client.send_message() method. See the Send API reference for full details.
use waproto::whatsapp as wa;
let message = wa::Message {
conversation: Some("Hello subscribers!".to_string()),
..Default::default()
};
// Pass a newsletter JID directly to send_message
let msg_id = client.send_message(newsletter_jid, message).await?;
The library detects newsletter recipients automatically and sends messages as plaintext (no Signal encryption), with the correct type and mediatype stanza attributes inferred from the message content. Stanza-level <meta> nodes (for polls, events, etc.) are also included automatically, matching WhatsApp Web behavior.
Newsletter::send_message() was removed. Use client.send_message() instead — it accepts newsletter, group, and direct message JIDs.
send_reaction
Send a reaction to a newsletter message.
pub async fn send_reaction(
&self,
jid: &Jid,
server_id: u64,
reaction: &str,
) -> Result<(), NewsletterError>
Parameters:
jid — Newsletter JID
server_id — Server-assigned ID of the message to react to
reaction — Emoji code (e.g., "👍", "❤️"), or empty string to remove
Example:
// Add a reaction
client.newsletter()
.send_reaction(&newsletter_jid, server_id, "👍")
.await?;
// Remove a reaction
client.newsletter()
.send_reaction(&newsletter_jid, server_id, "")
.await?;
edit_message
Edit a previously-sent newsletter message. Channel messages are plaintext, so the edit is sent as a <message edit="1"> stanza with the new protobuf body — not through the E2E send path.
pub async fn edit_message(
&self,
jid: &Jid,
message_id: impl Into<String>,
new_content: wa::Message,
) -> Result<(), NewsletterError>
Parameters:
jid — Newsletter JID. Non-newsletter JIDs are rejected with an error; use Client::edit_message for DMs and groups.
message_id — The target message’s message_id (the wire stanza id, as returned by send_message or carried on NewsletterMessage). This is not the server_id used by reactions. Empty IDs are rejected.
new_content — Replacement message body. Typically a wa::Message { conversation: Some(..), .. } for text edits.
Example:
use waproto::whatsapp as wa;
let new_body = wa::Message {
conversation: Some("edited text".to_string()),
..Default::default()
};
client.newsletter()
.edit_message(&newsletter_jid, original_message_id, new_body)
.await?;
revoke_message
Revoke (delete) a previously-sent newsletter message. Like edit_message, this goes through the plaintext channel path, not the E2E send path.
pub async fn revoke_message(
&self,
jid: &Jid,
message_id: impl Into<String>,
) -> Result<(), NewsletterError>
Parameters:
jid — Newsletter JID. Non-newsletter JIDs are rejected; use Client::revoke_message for DMs and groups.
message_id — The target message’s message_id (wire stanza id, not server_id). Empty IDs are rejected.
Example:
client.newsletter()
.revoke_message(&newsletter_jid, message_id)
.await?;
Newsletter JIDs are now also rejected at the root of the E2E send path. If your code accidentally routes a channel JID through send_message_impl, pin_message, or the standard edit_message / revoke_message on Client, you get an error that names the mis-route instead of a malformed encrypted fan-out.
get_messages
Fetch message history from a newsletter.
pub async fn get_messages(
&self,
jid: &Jid,
count: u32,
before: Option<u64>,
) -> Result<Vec<NewsletterMessage>, NewsletterError>
Parameters:
jid — Newsletter JID
count — Maximum number of messages to return
before — If set, return messages before this server_id (for pagination)
Returns:
Vec<NewsletterMessage> — List of newsletter messages
Example:
// Fetch latest 50 messages
let messages = client.newsletter()
.get_messages(&newsletter_jid, 50, None)
.await?;
// Paginate backwards
if let Some(oldest) = messages.last() {
let older = client.newsletter()
.get_messages(&newsletter_jid, 50, Some(oldest.server_id))
.await?;
}
subscribe_live_updates
Subscribe to live updates for a newsletter (reaction counts, message changes).
pub async fn subscribe_live_updates(
&self,
jid: &Jid,
) -> Result<u64, NewsletterError>
Parameters:
Returns:
u64 — Subscription duration in seconds (typically 300)
The server sends Event::NewsletterLiveUpdate events with updated reaction counts. You need to re-subscribe periodically when the duration expires.
Example:
let duration = client.newsletter()
.subscribe_live_updates(&newsletter_jid)
.await?;
println!("Subscribed for {}s", duration);
Types
Metadata for a newsletter channel. Implements PartialEq and Eq for direct comparison.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewsletterMetadata {
pub jid: Jid,
pub name: String,
pub description: Option<String>,
pub subscriber_count: u64,
pub verification: NewsletterVerification,
pub state: NewsletterState,
pub picture_url: Option<String>,
pub preview_url: Option<String>,
pub invite_code: Option<String>,
pub role: Option<NewsletterRole>,
pub creation_time: Option<u64>,
}
NewsletterVerification
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NewsletterVerification {
Verified,
Unverified,
}
NewsletterState
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NewsletterState {
Active,
Suspended,
Geosuspended,
}
NewsletterRole
The viewer’s role in a newsletter.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NewsletterRole {
Owner,
Admin,
Subscriber,
Guest,
}
All newsletter enums are #[non_exhaustive], so match statements should include a wildcard arm to handle future variants.
NewsletterMessage
A message from a newsletter’s history.
pub struct NewsletterMessage {
/// Server-assigned message ID (monotonic, used for pagination cursors).
pub server_id: u64,
/// Message timestamp (Unix seconds).
pub timestamp: u64,
/// Message type (Text, Media, Reaction, etc.).
pub message_type: NewsletterMessageType,
/// Whether the viewer is the sender.
pub is_sender: bool,
/// Decoded protobuf message (from plaintext bytes).
pub message: Option<wa::Message>,
/// Reaction counts on this message.
pub reactions: Vec<NewsletterReactionCount>,
}
NewsletterMessageType
The type of a newsletter message. Uses a StringEnum for type-safe wire-protocol mapping. Implements PartialEq and Eq.
#[non_exhaustive]
pub enum NewsletterMessageType {
Text, // "text"
Media, // "media"
Reaction, // "reaction"
Revoke, // "revoke"
PollCreation, // "poll_creation"
PollVote, // "poll_vote"
Edit, // "edit"
Other(String), // Unknown/future types
}
Methods:
as_str() - Returns the wire-protocol string representation
From<&str> - Parse from wire string, unknown values become Other(String)
NewsletterReactionCount
A reaction count on a newsletter message.
pub struct NewsletterReactionCount {
pub code: String,
pub count: u64,
}
Error handling
All newsletter methods return Result<T, NewsletterError>:
#[non_exhaustive]
pub enum NewsletterError {
#[error(transparent)]
Mex(#[from] MexError),
#[error(transparent)]
Iq(#[from] IqError),
#[error(transparent)]
Client(#[from] ClientError),
#[error("invalid newsletter request: {0}")]
InvalidRequest(String),
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
use whatsapp_rust::NewsletterError;
match client.newsletter().join(&newsletter_jid).await {
Ok(metadata) => println!("Joined: {}", metadata.jid),
Err(NewsletterError::Mex(e)) => eprintln!("MEX error: {}", e),
Err(NewsletterError::Iq(e)) => eprintln!("IQ error: {}", e),
Err(e) => eprintln!("Error: {}", e),
}