Skip to main content
This guide covers sending messages, working with different content types, and managing message delivery in LibXMTP groups.

Sending Text Messages

Basic Message Sending

Send a simple text message:
use xmtp_mls::groups::send_message_opts::SendMessageOpts;
use xmtp_content_types::text::TextCodec;
use xmtp_content_types::ContentCodec;

// Encode text content
let text_content = TextCodec::encode("Hello, world!".to_string())?;
let encoded_bytes = encoded_content_to_bytes(text_content)?;

// Send the message
let message_id = group.send_message(&encoded_bytes, SendMessageOpts::default()).await?;
Messages are sent as encoded content that specifies the content type. This allows receivers to properly decode and display the message.

Send Message Options

Control message behavior with SendMessageOpts:
use xmtp_mls::groups::send_message_opts::SendMessageOptsBuilder;

// Configure send options
let opts = SendMessageOptsBuilder::default()
    .should_push(true)  // Trigger push notifications
    .build()?;

let message_id = group.send_message(&encoded_bytes, opts).await?;

Optimistic Sending

Send messages without waiting for network confirmation:
// Message is stored locally and queued for sending
let message_id = group.send_message_optimistic(
    &encoded_bytes,
    SendMessageOpts::default()
)?;

// Later, publish all pending messages
group.publish_messages().await?;
Optimistic messages are visible locally immediately but may fail to send. Always call publish_messages() to ensure delivery.

Content Types

LibXMTP supports multiple content types for rich messaging.

Text Content

use xmtp_content_types::text::TextCodec;

let content = TextCodec::encode("Hello!".to_string())?;
let bytes = encoded_content_to_bytes(content)?;

group.send_message(&bytes, SendMessageOpts::default()).await?;

Reactions

Send reactions to messages:
use xmtp_content_types::reaction::{ReactionCodec, ReactionAction};
use xmtp_proto::xmtp::mls::message_contents::content_types::ReactionV2;

let reaction = ReactionV2 {
    reference: hex::encode(&target_message_id),  // Message being reacted to
    action: ReactionAction::Added as i32,
    content: "👍".to_string(),
    schema: 1,
};

let encoded = ReactionCodec::encode(reaction)?;
let bytes = encoded_content_to_bytes(encoded)?;

group.send_message(&bytes, SendMessageOpts::default()).await?;

Replies

Reply to specific messages:
use xmtp_content_types::reply::{ReplyCodec, Reply};

let reply = Reply {
    reference: hex::encode(&original_message_id),
    content_type: Some(TextCodec::content_type()),
    content: TextCodec::encode("Thanks!".to_string())?,
};

let encoded = ReplyCodec::encode(reply)?;
let bytes = encoded_content_to_bytes(encoded)?;

group.send_message(&bytes, SendMessageOpts::default()).await?;

Delete Messages

Delete your own messages or (as super admin) others’ messages:
use xmtp_proto::xmtp::mls::message_contents::content_types::DeleteMessage;
use xmtp_content_types::delete_message::DeleteMessageCodec;

let delete_msg = DeleteMessage {
    message_id: hex::encode(&message_id_to_delete),
};

let encoded = DeleteMessageCodec::encode(delete_msg)?;
let bytes = encoded_content_to_bytes(encoded)?;

let deletion_id = group.send_message(&bytes, SendMessageOpts::default()).await?;
Or use the helper method:
// Delete a message (validates permissions automatically)
let deletion_id = group.delete_message(&message_id).await?;

Reading Messages

1
Query Messages
2
Retrieve messages with filters:
3
use xmtp_db::group_message::MsgQueryArgs;

let args = MsgQueryArgs::default()
    .sent_after_ns(Some(start_time))
    .sent_before_ns(Some(end_time))
    .limit(Some(50));

let messages = group.find_messages(&args)?;

for msg in messages {
    println!("From: {}", msg.sender_inbox_id);
    println!("At: {}", msg.sent_at_ns);
    // Decode msg.decrypted_message_bytes based on content_type
}
4
Get Enriched Messages
5
Retrieve messages with reactions, replies, and deletion status:
6
let enriched_messages = group.find_enriched_messages(&args)?;

for msg in enriched_messages {
    println!("Message: {}", msg.id);
    println!("Reactions: {}", msg.reactions.len());
    
    if msg.is_deleted {
        println!("  [DELETED]");
    }
    
    if let Some(reply_id) = msg.reply_to_message_id {
        println!("  Reply to: {}", reply_id);
    }
}
7
Get Single Message
8
Look up by message ID:
9
// Get stored message
let message = client.message(message_id.clone())?;

// Get enriched message
let enriched = client.message_v2(message_id)?;

Message Delivery Status

Check Delivery Status

use xmtp_db::group_message::DeliveryStatus;

let messages = group.find_messages(&MsgQueryArgs::default())?;

for msg in messages {
    match msg.delivery_status {
        DeliveryStatus::Published => {
            println!("Message sent successfully");
        }
        DeliveryStatus::Unpublished => {
            println!("Message pending (optimistic send)");
        }
        DeliveryStatus::Failed => {
            println!("Message failed to send");
        }
    }
}

Publish Pending Messages

Send all locally queued messages:
// Publish optimistic messages
group.publish_messages().await?;

// Check if any messages failed
let failed_args = MsgQueryArgs::default()
    .delivery_status(Some(vec![DeliveryStatus::Failed]));

let failed = group.find_messages(&failed_args)?;
if !failed.is_empty() {
    eprintln!("Warning: {} messages failed to send", failed.len());
}

Advanced Usage

Custom Content Types

Define your own content type:
use xmtp_proto::xmtp::mls::message_contents::EncodedContent;
use prost::Message;

// Your custom message structure
#[derive(Message)]
struct CustomMessage {
    #[prost(string, tag = "1")]
    pub custom_field: String,
}

// Encode as EncodedContent
let custom = CustomMessage {
    custom_field: "value".to_string(),
};

let mut content_bytes = Vec::new();
custom.encode(&mut content_bytes)?;

let encoded = EncodedContent {
    r#type: Some(ContentTypeId {
        authority_id: "your.authority".to_string(),
        type_id: "custom-type".to_string(),
        version_major: 1,
        version_minor: 0,
    }),
    content: content_bytes,
    ..Default::default()
};

let mut bytes = Vec::new();
encoded.encode(&mut bytes)?;

group.send_message(&bytes, SendMessageOpts::default()).await?;

Message Count

Count messages matching criteria:
let count = group.count_messages(&MsgQueryArgs::default())?;
println!("Total messages: {}", count);

// Count unread messages
let unread_args = MsgQueryArgs::default()
    .sent_after_ns(Some(last_read_timestamp));

let unread_count = group.count_messages(&unread_args)?;

Read Receipts

Get last read times by sender:
let read_times = group.get_last_read_times()?;

for (sender_inbox_id, timestamp) in read_times {
    println!("{} last read at {}", sender_inbox_id, timestamp);
}

TypeScript (Node.js) Examples

import { Conversation } from '@xmtp/node-bindings'

// Send text message
const messageId = await conversation.sendText('Hello, world!')

// Send with optimistic delivery
const messageId = await conversation.sendText('Hello!', true)

// Publish pending messages
await conversation.publishMessages()

Error Handling

use xmtp_mls::groups::GroupError;

match group.send_message(&bytes, opts).await {
    Ok(message_id) => {
        println!("Message sent: {}", hex::encode(message_id));
    }
    Err(GroupError::InvalidPermission) => {
        eprintln!("You don't have permission to send messages");
    }
    Err(GroupError::NotFound(NotFound::MlsGroup)) => {
        eprintln!("Group not found locally, try syncing");
    }
    Err(e) => {
        eprintln!("Failed to send: {}", e);
    }
}

Best Practices

1
Use Optimistic Sending Carefully
2
Optimistic sending improves UX but requires careful handling:
3
// Send optimistically
let msg_id = group.send_message_optimistic(&bytes, opts)?;

// Always publish later
match group.publish_messages().await {
    Ok(_) => { /* success */ }
    Err(e) => {
        // Handle failure - maybe retry or notify user
        eprintln!("Failed to publish: {}", e);
    }
}
4
Sync Before Reading
5
Always sync to get latest messages:
6
group.sync().await?;
let messages = group.find_messages(&MsgQueryArgs::default())?;
7
Handle Large Message Lists
8
Use pagination for performance:
9
let page_size = 50;
let mut offset = 0;

loop {
    let args = MsgQueryArgs::default()
        .limit(Some(page_size));
        
    let messages = group.find_messages(&args)?;
    
    if messages.is_empty() {
        break;
    }
    
    // Process messages
    process_messages(messages);
    
    offset += page_size;
}
10
Decode Content Types Properly
11
Always check content type before decoding:
12
use xmtp_db::group_message::ContentType;

for msg in messages {
    match msg.content_type {
        ContentType::Text => {
            let content = TextCodec::decode(msg.decrypted_message_bytes)?;
            println!("Text: {}", content);
        }
        ContentType::Reaction => {
            let reaction = ReactionV2::decode(msg.decrypted_message_bytes.as_slice())?;
            println!("Reaction: {}", reaction.content);
        }
        _ => {
            println!("Unknown content type");
        }
    }
}

Next Steps

Permissions and Policies

Control who can send messages and perform actions

Consent Management

Manage user consent and block unwanted messages

Build docs developers (and LLMs) love