Skip to main content

Overview

LibXMTP uses a codec system to encode and decode different types of message content. Each content type has a unique identifier and version, following the format:
authority_id/type_id:version_major.version_minor
Example: xmtp.org/text:1.0

Standard content types

All standard content types use xmtp.org as the authority ID.

Text

Plain text messages with UTF-8 encoding. Type ID: xmtp.org/text:1.0 Structure:
pub struct Text {
    pub content: String,
}
Encoding (Node.js):
import { encodeText } from '@xmtp/node-bindings'

const encoded = encodeText('Hello, world!')
await conversation.send(encoded, { shouldPush: true })
Decoding:
if (message.content.type === DecodedMessageContentType.Text) {
  console.log(message.content.text)
}
Triggers push: Yes

Markdown

Markdown-formatted text for rich content. Type ID: xmtp.org/markdown:1.0 Structure:
pub struct Markdown {
    pub content: String,
}
Encoding (Node.js):
import { encodeMarkdown } from '@xmtp/node-bindings'

const encoded = encodeMarkdown('# Heading\n\n**Bold text**')
await conversation.send(encoded, { shouldPush: true })
Triggers push: Yes

Reaction

Emoji or text reactions to messages. Type ID: xmtp.org/reaction:2.0 Structure:
pub struct ReactionV2 {
    pub reference: String,              // Message ID being reacted to
    pub reference_inbox_id: String,     // Inbox ID of message author
    pub action: ReactionAction,         // Added or Removed
    pub content: String,                // Emoji or text content
    pub schema: ReactionSchema,         // Unicode, Shortcode, or Custom
}

pub enum ReactionAction {
    Unspecified = 0,
    Added = 1,
    Removed = 2,
}

pub enum ReactionSchema {
    Unspecified = 0,
    Unicode = 1,      // Standard emoji: 👍
    Shortcode = 2,    // Shortcode format: :thumbsup:
    Custom = 3,       // Custom reaction format
}
Encoding (Node.js):
import { encodeReaction, ReactionAction, ReactionSchema } from '@xmtp/node-bindings'

const encoded = encodeReaction({
  reference: messageId,
  referenceInboxId: senderInboxId,
  action: ReactionAction.Added,
  content: '👍',
  schema: ReactionSchema.Unicode
})

await conversation.send(encoded, { shouldPush: false })
Fallback text: "Reacted with \"👍\" to an earlier message" Triggers push: No Legacy support: Version 1.0 uses JSON encoding, automatically converted to v2

Reply

Threaded replies to previous messages. Type ID: xmtp.org/reply:1.0 Structure:
pub struct Reply {
    pub in_reply_to: Option<Box<DecodedMessage>>,  // Referenced message (enriched only)
    pub content: Box<MessageBody>,                  // Reply content
    pub reference_id: String,                       // Message ID being replied to
}
Encoding (Node.js):
import { encodeReply, encodeText } from '@xmtp/node-bindings'

const replyContent = encodeText('Great point!')
const encoded = encodeReply({
  reference: originalMessageId,
  referenceInboxId: originalSenderInboxId,
  content: replyContent
})

await conversation.send(encoded, { shouldPush: true })
Enrichment: When using listEnrichedMessages(), the in_reply_to field is populated with the referenced message. Fallback text: "Replied with \"Great point!\" to an earlier message" (for text replies) Triggers push: Yes

Attachment

Direct file attachments with content embedded. Type ID: xmtp.org/attachment:1.0 Structure:
pub struct Attachment {
    pub filename: Option<String>,
    pub mime_type: String,
    pub content: Vec<u8>,  // Raw file bytes
}
Encoding (Node.js):
import { encodeAttachment } from '@xmtp/node-bindings'
import { readFileSync } from 'fs'

const fileData = readFileSync('document.pdf')
const encoded = encodeAttachment({
  filename: 'document.pdf',
  mimeType: 'application/pdf',
  content: fileData
})

await conversation.send(encoded, { shouldPush: true })
Fallback text: "Can't display document.pdf. This app doesn't support attachments." Triggers push: Yes Note: For large files, use RemoteAttachment instead.

RemoteAttachment

Reference to externally stored file. Type ID: xmtp.org/remoteAttachment:1.0 Structure:
pub struct RemoteAttachment {
    pub url: String,
    pub content_digest: Vec<u8>,
    pub secret: Vec<u8>,
    pub salt: Vec<u8>,
    pub nonce: Vec<u8>,
    pub scheme: String,
    pub filename: Option<String>,
    pub content_length: Option<u64>,
}
Usage: Upload file to external storage (IPFS, S3, etc.), then send reference. Triggers push: Yes

MultiRemoteAttachment

Multiple remote file references in a single message. Type ID: xmtp.org/multiRemoteAttachment:1.0 Structure:
pub struct MultiRemoteAttachment {
    pub attachments: Vec<RemoteAttachment>,
}
Triggers push: Yes

ReadReceipt

Indicates message has been read. Type ID: xmtp.org/readReceipt:1.0 Structure:
pub struct ReadReceipt {}
Encoding (Node.js):
import { encodeReadReceipt } from '@xmtp/node-bindings'

const encoded = encodeReadReceipt()
await conversation.send(encoded, { shouldPush: false })
Triggers push: No Note: Empty payload, presence indicates receipt.

TransactionReference

Reference to blockchain transaction. Type ID: xmtp.org/transactionReference:1.0 Structure:
pub struct TransactionReference {
    pub chain_id: u64,
    pub namespace: String,       // e.g., "eip155"
    pub reference: String,       // Transaction hash
    pub metadata: HashMap<String, String>,
}
Encoding (Node.js):
import { encodeTransactionReference } from '@xmtp/node-bindings'

const encoded = encodeTransactionReference({
  chainId: 1,
  namespace: 'eip155',
  reference: '0x123abc...',
  metadata: { amount: '1.5 ETH' }
})

await conversation.send(encoded, { shouldPush: true })
Triggers push: Yes

WalletSendCalls

Wallet function call data (EIP-5792). Type ID: xmtp.org/walletSendCalls:1.0 Structure:
pub struct WalletSendCalls {
    pub version: String,
    pub chain_id: String,
    pub calls: Vec<SendCallsParams>,
}
Triggers push: Yes

GroupUpdated

Group metadata change notification. Type ID: xmtp.org/groupUpdated:1.0 Structure:
pub struct GroupUpdated {
    pub metadata_field_changes: Vec<MetadataFieldChange>,
}
Triggers push: No Note: Automatically sent when group name, image, or description changes.

LeaveRequest

Request to leave a group. Type ID: xmtp.org/leaveRequest:1.0 Structure:
pub struct LeaveRequest {}
Triggers push: No

Intent

User intent or action. Type ID: xmtp.org/intent:1.0 Structure:
pub struct Intent {
    pub intent_type: String,
    pub parameters: HashMap<String, String>,
}
Triggers push: Varies

Actions

Interactive action buttons or options. Type ID: xmtp.org/actions:1.0 Structure:
pub struct Actions {
    pub actions: Vec<Action>,
}
Triggers push: Varies

System content types

DeletedMessage

Placeholder for deleted messages (read-only). Type ID: xmtp.org/deletedMessage:1.0 Structure:
MessageBody::DeletedMessage {
    deleted_by: DeletedBy,
}

pub enum DeletedBy {
    Sender,
    Admin(String),  // Admin's inbox_id
}
Note: Cannot be sent directly. Created automatically when messages are deleted.

Custom content types

If LibXMTP encounters an unknown content type, it wraps it as MessageBody::Custom:
MessageBody::Custom(EncodedContent)
Accessing custom content:
if (message.content.type === DecodedMessageContentType.Custom) {
  const encoded = message.content.custom
  const contentType = encoded.type
  const parameters = encoded.parameters
  const content = encoded.content
  
  // Handle custom decoding
}

ContentCodec trait

All content types implement the ContentCodec trait:
pub trait ContentCodec<T> {
    fn content_type() -> ContentTypeId;
    fn encode(content: T) -> Result<EncodedContent, CodecError>;
    fn decode(content: EncodedContent) -> Result<T, CodecError>;
    fn should_push() -> bool;
}

Implementing custom codecs

use xmtp_content_types::{ContentCodec, CodecError};
use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};

pub struct CustomCodec;

impl CustomCodec {
    const AUTHORITY_ID: &'static str = "example.com";
    pub const TYPE_ID: &'static str = "custom";
    pub const MAJOR_VERSION: u32 = 1;
    pub const MINOR_VERSION: u32 = 0;
}

impl ContentCodec<MyCustomType> for CustomCodec {
    fn content_type() -> ContentTypeId {
        ContentTypeId {
            authority_id: Self::AUTHORITY_ID.to_string(),
            type_id: Self::TYPE_ID.to_string(),
            version_major: Self::MAJOR_VERSION,
            version_minor: Self::MINOR_VERSION,
        }
    }
    
    fn encode(data: MyCustomType) -> Result<EncodedContent, CodecError> {
        // Encode logic
    }
    
    fn decode(content: EncodedContent) -> Result<MyCustomType, CodecError> {
        // Decode logic
    }
    
    fn should_push() -> bool {
        true
    }
}

Content type detection

Check content type before accessing fields:
switch (message.content.type) {
  case DecodedMessageContentType.Text:
    return message.content.text
    
  case DecodedMessageContentType.Reaction:
    const reaction = message.content.reaction!
    console.log(`${reaction.action}: ${reaction.content}`)
    break
    
  case DecodedMessageContentType.Reply:
    const reply = message.content.reply!
    console.log(`Reply to ${reply.reference}:`, reply.content)
    break
    
  case DecodedMessageContentType.Attachment:
    const attachment = message.content.attachment!
    saveFile(attachment.filename, attachment.content)
    break
    
  case DecodedMessageContentType.DeletedMessage:
    const deleted = message.content.deletedMessage!
    if (deleted.deletedBy === 'sender') {
      console.log('Message deleted by sender')
    } else {
      console.log(`Message deleted by admin: ${deleted.adminInboxId}`)
    }
    break
}

Fallback text

Most content types include fallback text for clients that don’t support them:
if (!supportsContentType(message.contentType)) {
  console.log(message.fallback)  // "Replied to an earlier message"
}
Fallback text is automatically generated during encoding based on content type.

Source code references

  • Rust content types: crates/xmtp_content_types/src/
  • Node.js bindings: bindings/node/src/content_types/
  • Core codec trait: crates/xmtp_content_types/src/lib.rs:94
  • Message body enum: crates/xmtp_mls/src/messages/decoded_message.rs:61

Build docs developers (and LLMs) love