Skip to content

Understand content types with XMTP

XMTP application messages carry a content type. The type tells an app how to decode the payload.

The payload is bytes. It can carry text, JSON, or binary content. Apps must agree on the type and its encoding.

Every content type ID has the form authorityId/typeId:versionMajor.versionMinor. The encoded envelope contains the type ID, parameters, content bytes, and optional fallback text. shouldPush is a send option, separate from the content envelope. The table gives each type’s default. An explicit send option can override that default.

Content typeType IDWire payloadFallbackDefault shouldPush
Textxmtp.org/text:1.0UTF-8 bytes and an encoding parameterNonetrue
Markdownxmtp.org/markdown:1.0UTF-8 bytes and an encoding parameterNonetrue
Replyxmtp.org/reply:1.0Nested EncodedContent; reference parametersDescribes the replytrue
Reactionxmtp.org/reaction:2.0ReactionV2 protobufDescribes the reactionfalse
Read receiptxmtp.org/readReceipt:1.0EmptyNonefalse
Attachmentxmtp.org/attachment:1.0Raw bytes; MIME type and file name parametersNames the unsupported filetrue
Remote attachmentxmtp.org/remoteStaticAttachment:1.0URL bytes and decryption parametersNames the unsupported filetrue
Multiple remote attachmentsxmtp.org/multiRemoteStaticAttachment:1.0MultiRemoteAttachment protobufDescribes unsupported attachmentstrue
Group updatexmtp.org/group_updated:1.0GroupUpdated protobufNonefalse
Transaction referencexmtp.org/transactionReference:1.0JSON-encoded TransactionReferenceDescribes the transactiontrue
Wallet send callsxmtp.org/walletSendCalls:1.0JSON-encoded WalletSendCallsDescribes the requesttrue
Delete messagexmtp.org/deleteMessage:1.0DeleteMessage protobufNonefalse
Leave requestxmtp.org/leave_request:1.0LeaveRequest protobufNonefalse
Actionscoinbase.com/actions:1.0JSON-encoded ActionsNumbered action listtrue
Intentcoinbase.com/intent:1.0JSON-encoded IntentNames the selected actiontrue

Browser, Node, Kotlin, and Swift include these standard codecs. Core decodes standard content without app registration. Register codecs only for custom content that your app receives. A codec matches the authority, type ID, and major version; the minor version is not part of this match.

Node exports standalone codecs from @xmtp/node-sdk. Browser exports them from @xmtp/browser-sdk/pure; call initPureWasm() before you use them. Kotlin and Swift expose typed codec wrappers over the same Rust encoders and decoders.

Group updates are local transcripts produced from commits. Apps cannot publish a group-update transcript as an application message. Delete messages are control messages; Core applies authorized deletion and exposes the result as deleted-message content. Use the SDK delete methods to request deletion.

When building with XMTP, you can’t know in advance whether a recipient’s app will support a given content type, especially a custom one. Likewise, your own app might receive messages with content types it doesn’t support.

To prevent a poor user experience or app crashes, you should use the fallback property.

For sending: When sending a message with a custom content type, always provide a fallback string. This string offers a human-readable representation of the content. If the recipient’s app doesn’t support your custom type, it can display the fallback text instead.

For receiving: When your app receives a message, check if it supports the message’s contentType. If not, render the fallback text.

Some background types, such as read receipts, have no fallback. A renderer can skip them. Do not assume that message.content is missing for an unsupported type. Browser and Node expose a content union with unknown and custom variants. Kotlin and Swift expose SDKMessageContent with standard, custom, and unknown variants. Raw bytes and decode errors remain available.

These examples show a text-only renderer. It shows text, skips read receipts, and uses fallback text for other content. Extend it with the types your app can display.

import type {
class Message

A received or stored message. Its fields are public values. Its actions use the client that returned it; the message holds that client weakly, so an action after the client ends or is collected fails with ClientClosed. The generated base holds the message's fields.

Message
} from "@xmtp/browser-sdk";
export function
function displayText(message: Message): string | undefined
displayText
(
message: Message
message
:
class Message

A received or stored message. Its fields are public values. Its actions use the client that returned it; the message holds that client weakly, so an action after the client ends or is collected fails with ClientClosed. The generated base holds the message's fields.

Message
): string | undefined {
switch (
message: Message
message
.
Message.content: MessageContent
content
.
kind: "text" | "markdown" | "readReceipt" | "reaction" | "attachment" | "remoteAttachment" | "multiRemoteAttachment" | "transactionReference" | "walletSendCalls" | "actions" | "intent" | "groupUpdated" | "leaveRequest" | "deletedMessage" | "reply" | "custom" | "unknown"
kind
) {
case "text":
return
message: Message
message
.
Message.content: {
readonly kind: "text";
readonly value: string;
}
content
.
value: string
value
;
case "readReceipt":
return
var undefined
undefined
;
default:
return
message: Message
message
.
MessageFields.fallback?: string | undefined
fallback
;
}
}

Custom content types allow you to define your own schemas for messages that go beyond what is covered by standard content types. These are useful for experiments, domain-specific features, or app-specific behaviors.

A content type needs a codec which must satisfy the ContentCodec interface from @xmtp/node-sdk or @xmtp/browser-sdk.

Example:

import type {
interface ContentCodec<T>

A content codec over public values. The send helpers run its steps before the send starts: encode, then fallback when the envelope has none, then shouldPush when the send has no explicit shouldPush option. A step that throws, rejects, or returns a value of the wrong type fails the send with XmtpError.CodecEncodeFailed, and the SDK makes no publish attempt. Each step is a function property, so the value type is checked in both directions.

ContentCodec
,
type ContentTypeId = {
readonly authorityId: string;
readonly typeId: string;
readonly versionMajor: number;
readonly versionMinor: number;
}
ContentTypeId
,
type EncodedContent = {
readonly type: ContentTypeId;
readonly parameters?: Map<string, string>;
readonly fallback?: string | undefined;
readonly content: Uint8Array;
}
EncodedContent
,
} from "@xmtp/node-sdk";
// Define the content type identifier.
export const
const CustomContentType: ContentTypeId
CustomContentType
:
type ContentTypeId = {
readonly authorityId: string;
readonly typeId: string;
readonly versionMajor: number;
readonly versionMinor: number;
}
ContentTypeId
= {
authorityId: string
authorityId
: "your-domain.com",
typeId: string
typeId
: "your-custom-id",
versionMajor: number
versionMajor
: 1,
versionMinor: number
versionMinor
: 0,
};
// Implement a codec for string values.
export class
class CustomCodec
CustomCodec
implements
interface ContentCodec<T>

A content codec over public values. The send helpers run its steps before the send starts: encode, then fallback when the envelope has none, then shouldPush when the send has no explicit shouldPush option. A step that throws, rejects, or returns a value of the wrong type fails the send with XmtpError.CodecEncodeFailed, and the SDK makes no publish attempt. Each step is a function property, so the value type is checked in both directions.

ContentCodec
<string> {
readonly
CustomCodec.type: ContentTypeId
type
=
const CustomContentType: ContentTypeId
CustomContentType
;
CustomCodec.encode(content: string): EncodedContent
encode
(
content: string
content
: string):
type EncodedContent = {
readonly type: ContentTypeId;
readonly parameters?: Map<string, string>;
readonly fallback?: string | undefined;
readonly content: Uint8Array;
}
EncodedContent
{
return {
type: ContentTypeId
type
: this.
CustomCodec.type: ContentTypeId
type
,
parameters?: Map<string, string> | undefined
parameters
: new
var Map: MapConstructor
new () => Map<any, any> (+3 overloads)
Map
(),
content: Uint8Array<ArrayBufferLike>
content
: new
var TextEncoder: new () => TextEncoder

The TextEncoder interface enables you to encode a JavaScript string using UTF-8.

MDN Reference

TextEncoder
().
TextEncoder.encode(input?: string): Uint8Array<ArrayBuffer>

The TextEncoder.encode() method takes a string as input, and returns a Uint8Array containing the string encoded using UTF-8.

MDN Reference

encode
(
content: string
content
),
fallback?: string | undefined
fallback
:
content: string
content
,
};
}
CustomCodec.decode(content: EncodedContent): string
decode
(
content: EncodedContent
content
:
type EncodedContent = {
readonly type: ContentTypeId;
readonly parameters?: Map<string, string>;
readonly fallback?: string | undefined;
readonly content: Uint8Array;
}
EncodedContent
): string {
return new
var TextDecoder: new (label?: string, options?: TextDecoderOptions) => TextDecoder

The TextDecoder interface represents a decoder for a specific text encoding, such as UTF-8, ISO-8859-2, or GBK. A decoder takes an array of bytes as input and returns a JavaScript string.

MDN Reference

TextDecoder
().
TextDecoder.decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string

The TextDecoder.decode() method returns a string containing text decoded from the buffer passed as a parameter.

MDN Reference

decode
(
content: EncodedContent
content
.
content: Uint8Array<ArrayBufferLike>
content
);
}
CustomCodec.fallback(content: string): string | undefined

Text for recipients without this codec. Absent: no fallback.

fallback
(
content: string
content
: string): string | undefined {
return
content: string
content
;
}
CustomCodec.shouldPush(): boolean

Whether sending this value notifies recipients. Absent: it does.

shouldPush
(): boolean {
return false;
}
}

Pass custom codec instances in the codecs option to Browser or Node Client.create(), or to Agent.create(). Kotlin and Swift accept a codecs argument on SDKClient.create(). Registration decodes incoming custom content. To send a custom value, pass the codec and value to a typed send method.

This example registers a codec with an agent:

import {
class Agent<ContentTypes = unknown>

Event-driven XMTP agent that routes conversations and messages to middleware.

Agent
} from "@xmtp/agent-sdk";
// Set XMTP_WALLET_KEY and XMTP_BACKEND_URL before this program runs.
const
const agent: Agent<unknown>
agent
= await
class Agent<ContentTypes = unknown>

Event-driven XMTP agent that routes conversations and messages to middleware.

Agent
.
Agent<ContentTypes = unknown>.createFromEnv<CustomCodec[]>(options?: Partial<AgentCreateOptions<CustomCodec[]>> | undefined): Promise<Agent<unknown>>

Create an agent from XMTP_* variables. XMTP_BACKEND_URL overrides options.backend; one must be supplied. Pass backend.credentials in options for backend authentication.

createFromEnv
({
codecs?: CustomCodec[] | undefined

Custom content codecs registered with the client.

codecs
: [new
constructor CustomCodec(): CustomCodec
CustomCodec
()],
});