Skip to content

Understand content types with XMTP

When you build an app with XMTP, all messages are encoded with a content type to ensure that an XMTP client knows how to encode and decode messages, ensuring interoperability and consistent display of messages across apps.

In addition, message payloads are transported as a set of bytes. This means that payloads can carry any content type that a client supports, such as plain text, JSON, or even non-text binary or media content.

Every content type ID has the form authorityId/typeId:versionMajor.versionMinor. A message carries its encoded payload plus a fallback string and a shouldPush flag.

Content typeType IDPayloadFallbackshouldPushPlatforms
Textxmtp.org/text:1.0UTF-8 bytes and an encoding parameterNonetrueAll
Markdownxmtp.org/markdown:1.0UTF-8 bytes and an encoding parameterNonetrueBrowser, Node
Replyxmtp.org/reply:1.0Nested EncodedContent and a referenceText replies describe the reply targettrueAll
Reactionxmtp.org/reaction:2.0Reaction protobufDescribes the reaction and targetfalse in Rust; Kotlin and Swift use true for added reactionsAll
Read receiptxmtp.org/readReceipt:1.0EmptyNonefalseAll
Attachmentxmtp.org/attachment:1.0Raw bytes; MIME type and file name parametersNames the unsupported filetrueAll
Remote attachmentxmtp.org/remoteStaticAttachment:1.0URL bytes and decryption parametersNames the unsupported filetrueAll
Multiple remote attachmentsxmtp.org/multiRemoteStaticAttachment:1.0MultiRemoteAttachment protobufDescribes unsupported attachmentstrueAll
Group updatexmtp.org/group_updated:1.0GroupUpdated protobufNoneCodec: false; locally stored message: trueAll
Transaction referencexmtp.org/transactionReference:1.0TransactionReference protobufNonetrueAll
Wallet send callsxmtp.org/walletSendCalls:1.0WalletSendCalls protobufNonetrueBrowser, Node, Kotlin
Delete messagexmtp.org/deleteMessage:1.0Deleted message referenceNonefalseAll
Leave requestxmtp.org/leave_request:1.0LeaveRequest protobufNonefalseAll
Actionscoinbase.com/actions:1.0JSON-encoded ActionsNumbered action listtrueBrowser, Node
Intentcoinbase.com/intent:1.0JSON-encoded IntentNames the selected actiontrueBrowser, Node

Browser and Node register the listed codecs by default. Legacy Kotlin and Swift message APIs register text by default and can require Client.register(codec:) or options.codecs for other types. Enriched native message APIs decode a separate built-in set. Swift enriched messages can decode Markdown, Actions, Intent, and Wallet send calls even though Swift has no sender codec for them.

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.

However, some content types, especially those not meant for display (like read receipts), won’t have a fallback. In these undefined cases, you should generally ignore the message entirely. Displaying a generic “unsupported content” message for every silent background event would create a poor user experience and clutter the chat. The code examples below show how to handle both scenarios.

// if message content is undefined, it means the content type is not supported
// in this client
if (message.content === undefined) {
// return the fallback text, which may also be undefined
return message.fallback;
}

Kotlin and Swift do not expose the same missing-codec signal as JavaScript. Compare the public content type ID with the types that your renderer supports. A mobile registry can fall back to a text codec, so a registry lookup is not a valid support check.

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/content-type-primitives.

Example:

import type {
type ContentCodec<ContentType = unknown> = {
contentType: ContentTypeId;
encode(content: ContentType): EncodedContent;
decode(content: EncodedContent): ContentType;
fallback(content: ContentType): string | undefined;
shouldPush: (content: ContentType) => boolean;
}
ContentCodec
,
(alias) interface ContentTypeId
import ContentTypeId
ContentTypeId
,
(alias) interface EncodedContent
import EncodedContent
EncodedContent
,
} from '@xmtp/content-type-primitives';
// Define the content type identifier
export const
const CustomContentType: ContentTypeId
CustomContentType
:
(alias) interface ContentTypeId
import ContentTypeId
ContentTypeId
= {
ContentTypeId.authorityId: string
authorityId
: 'your-domain.com',
ContentTypeId.typeId: string
typeId
: 'your-custom-id',
ContentTypeId.versionMajor: number
versionMajor
: 1,
ContentTypeId.versionMinor: number
versionMinor
: 0,
};
// Implement the codec as a class
export class
class CustomCodec
CustomCodec
implements
type ContentCodec<ContentType = unknown> = {
contentType: ContentTypeId;
encode(content: ContentType): EncodedContent;
decode(content: EncodedContent): ContentType;
fallback(content: ContentType): string | undefined;
shouldPush: (content: ContentType) => boolean;
}
ContentCodec
<string> {
CustomCodec.contentType: ContentTypeId
contentType
=
const CustomContentType: ContentTypeId
CustomContentType
;
CustomCodec.encode(content: string): EncodedContent
encode
(
content: string
content
: string):
(alias) interface EncodedContent
import EncodedContent
EncodedContent
{
return {
EncodedContent.type?: ContentTypeId | undefined
type
: this.
CustomCodec.contentType: ContentTypeId
contentType
,
EncodedContent.parameters: Record<string, string>
parameters
: {},
EncodedContent.content: Uint8Array<ArrayBufferLike>
content
: new
var TextEncoder: new () => TextEncoder

The TextEncoder interface takes a stream of code points as input and emits a stream of UTF-8 bytes.

MDN Reference

TextEncoder class is a global reference for import { TextEncoder } from 'node:util' https://nodejs.org/api/globals.html#textencoder

@sincev11.0.0

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

The TextEncoder.encode() method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object.

MDN Reference

encode
(
content: string
content
),
};
}
CustomCodec.decode(content: EncodedContent): string
decode
(
content: EncodedContent
content
:
(alias) interface EncodedContent
import EncodedContent
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, KOI8-R, GBK, etc.

MDN Reference

TextDecoder class is a global reference for import { TextDecoder } from 'node:util' https://nodejs.org/api/globals.html#textdecoder

@sincev11.0.0

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
.
EncodedContent.content: Uint8Array<ArrayBufferLike>
content
);
}
CustomCodec.fallback(content: string): string | undefined
fallback
(
content: string
content
: string): string | undefined {
return
content: string
content
;
}
CustomCodec.shouldPush(): boolean
shouldPush
(): boolean {
return false;
}
}

Pass codec instances to Agent.create():

import {
class Agent<ContentTypes = unknown>

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

Agent
} from '@xmtp/agent-sdk';
const
const client: Agent<BuiltInContentTypes | EnrichedReply<BuiltInContentTypes, BuiltInContentTypes>>
client
= await
class Agent<ContentTypes = unknown>

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

Agent
.
Agent<ContentTypes = unknown>.create<CustomCodec[]>(signer: Parameters<(<ContentCodecs extends ContentCodec[] = []>(signer: Signer, options: DistributiveOmit<ClientOptions, "codecs"> & {
codecs?: ContentCodecs;
}) => Promise<Client<ExtractCodecContentTypes<ContentCodecs>>>)>[0], options: AgentCreateOptions<CustomCodec[]>): Promise<Agent<BuiltInContentTypes | EnrichedReply<...>>>

Create an agent and client. Device sync defaults to disabled for agents.

create
(
const signer: Signer
signer
, {
backendUrl: string

Backend URL, including the HTTP or HTTPS scheme.

backendUrl
,
codecs?: CustomCodec[] | undefined

Custom content codecs registered with the client.

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