Skip to content

Browser SDK Upgrade Guide

Start with Migrate to a self-hosted backend for the backend, database, history, and push steps. This guide lists changes from main at cc878025da to self-hosted at 610175f3b3. A call can keep its name and still have different parameters, results, or behavior. The Before examples use the old SDK. The After examples use the new SDK.

The package name stays @xmtp/browser-sdk. The self-hosted SDK uses ESM and generated public bindings. Its source moved from sdks/js/browser-sdk to sdks/browser. Remove imports from @xmtp/wasm-bindings and private package paths. Import client types from @xmtp/browser-sdk. Import standalone codecs and pure helpers from @xmtp/browser-sdk/pure.

Use SDK 8.0.0-rc1 with backend 8.0.0-rc1. Run this command from your app directory to save the exact SDK version:

Terminal window
npm install --save-exact @xmtp/browser-sdk@8.0.0-rc1

See Install the XMTP SDK for platform requirements.

The old signer used type, getIdentifier(), and signMessage(). The new signer uses asynchronous identity(), kind(), and sign(request). The signature result names its kind. Replace Identifier.identifierKind with PublicIdentity.kind.

Old API on main:

Before (main cc878025da)
import { Client, IdentifierKind, type Signer } from "@xmtp/browser-sdk";
async function createClient(
address: string,
signText: (text: string) => Promise<Uint8Array>,
) {
const signer: Signer = {
type: "EOA",
getIdentifier: () => ({
identifier: address,
identifierKind: IdentifierKind.Ethereum,
}),
signMessage: signText,
};
return Client.create(signer, { env: "dev", appVersion: "chat/1.0" });
}

New signer:

export function
function walletSigner(address: string, signText: (text: string) => Promise<Uint8Array>): Signer
walletSigner
(
address: string
address
: string,
signText: (text: string) => Promise<Uint8Array>
signText
: (
text: string
text
: string) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
>,
):
(alias) interface Signer
import Signer
Signer
{
return {
Signer.identity(): Promise<PublicIdentity>
identity
: async () => ({
identifier: string
identifier
:
address: string
address
,
kind: PublicIdentityKind
kind
: "ethereum" }),
Signer.kind(): Promise<SignerKind>
kind
: async () => ({
kind: "eoa"
kind
: "eoa" }),
Signer.sign(request: SigningRequest): Promise<Signature>
sign
: async (
request: SigningRequest
request
) => ({
kind: "ecdsa"
kind
: "ecdsa",
value: Uint8Array<ArrayBufferLike>
value
: await
signText: (text: string) => Promise<Uint8Array>
signText
(
request: SigningRequest
request
.
text: string
text
),
}),
};
}

New client:

export async function
function createClient(signer: Signer): Promise<Client>
createClient
(
signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
) {
return
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
Client.create(signer: Signer, options: ClientOptions): Promise<Client>
create
(
signer: Signer
signer
, {
backend?: BackendSource | undefined
backend
: {
url: string
url
: "https://xmtp.example.com",
appVersion?: string | undefined
appVersion
: "chat/1.0" },
storage: StorageOptions
storage
: {
location: StorageLocation
location
: "default" },
});
}

Use an explicit backend URL. env, apiUrl, and gatewayHost no longer select the network. The Browser SDK also requires storage. Client.create(signer, options) can establish an identity. Client.build(identity, options, inboxId?) opens a stored identity. An empty database fails with IdentityNotFound when opened with build.

For a smart contract wallet, return { kind: "scw", chainId, blockNumber } from kind(). Return { kind: "scw", bytes, address, chainId, blockNumber } from sign(). The optional block number and the chain ID use bigint. The new signer also supports the passkey kind. Remove createEOASigner, createSCWSigner, and toSafeSigner calls. Supply the new signer interface, or use Client.generateKeys() or LocalSigningKey.fromBytes(bytes) for an app-owned local key.

Old client optionSelf-hosted option or action
env, apiUrl, gatewayHostbackend: { url }. There is no environment-to-URL lookup.
backend: Backendbackend: BackendSource, with a connected Backend or { url, appVersion?, credentials? }. Backend.connect(options) replaces createBackend(options) and the old builder.
appVersionbackend.appVersion when using backend options.
dbPath: undefinedstorage: { location: "default" }. The new default layout differs.
dbPath: nullstorage: { location: "inMemory" }.
dbPath: stringstorage: { location: { dbPath, attachmentsDir } }. Both OPFS entry paths are required.
dbEncryptionKeyRemoved from the Browser API. Browser storage has no SDK database encryption key. The new SDK rejects storage.encryptionKey with StorageLocation; it does not ignore it.
disableDeviceSyncdeviceSync, with the opposite sense. The default is true.
disableAutoRegisterregistration: { auto: false }. The default is automatic registration.
Old client default inbox nonce 1nNew default is 0n. Set registration: { nonce: 1n } to preserve the old derived inbox ID. This does not transfer the old backend identity.
waitForRegistrationVisibleRemoved. There is no old node quorum option.
historySyncUrlRemoved. There is no archive-server URL option.
workerConfigworkers, with defaultIntervalNs and per-worker intervals records.
codecsStill per client, with the new public ContentCodec interface.
loggingLevel, structuredLogging, performanceLoggingUse the public logging configuration and setLogSink(). These are no longer client options.
No equivalentallowOffline, forkRecovery, handlers, and attachment limits in attachments.

The new SDK cannot open a pre-transition database. First create a new database on the target backend with Client.create. The examples below compare the reopen APIs; the new example requires a database already created by the new SDK. Old client.close() is synchronous. The new shutdown is await client.end(). Do not open two clients on one database at the same time.

Old API on main:

Before (main cc878025da)
import { Client, type Identifier } from "@xmtp/browser-sdk";
async function reopen(identity: Identifier, oldDbPath: string) {
const client = await Client.build(identity, { dbPath: oldDbPath });
client.close();
}

New explicit storage:

export async function
function reopenCurrentDatabase(identity: PublicIdentity, backend: BackendSource, currentDbPath: string, attachmentsDir: string): Promise<Client>
reopenCurrentDatabase
(
identity: PublicIdentity
identity
:
type PublicIdentity = {
readonly identifier: string;
readonly kind: PublicIdentityKind;
}
PublicIdentity
,
backend: BackendSource
backend
:
type BackendSource = BackendOptions | Backend
BackendSource
,
currentDbPath: string
currentDbPath
: string,
attachmentsDir: string
attachmentsDir
: string,
) {
// Use a database already created by this SDK. End its client first.
// Both paths name OPFS entries.
const
const options: ClientOptions
options
:
type ClientOptions = ClientOptions & {
readonly codecs?: readonly AnyContentCodec[];
}

Client options with the custom codecs that this client decodes.

ClientOptions
= {
backend?: BackendSource | undefined
backend
,
storage: StorageOptions
storage
: {
location: StorageLocation
location
: {
dbPath: string
dbPath
:
currentDbPath: string
currentDbPath
,
attachmentsDir: string
attachmentsDir
} },
};
const
const client: Client
client
= await
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
Client.build(identity: PublicIdentity, options: ClientOptions, inboxId?: InboxId): Promise<Client>
build
(
identity: PublicIdentity
identity
,
const options: ClientOptions
options
);
const
const inboxId: string
inboxId
=
const client: Client
client
.
ClientMembers.inboxId: string
inboxId
;
try {
if ((await
const client: Client
client
.
ClientMembers.storage: Storage
storage
.
Storage.path(): Promise<string | undefined>
path
()) !==
currentDbPath: string
currentDbPath
) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("The client opened another database");
}
} finally {
await
const client: Client
client
.
Client.end(): Promise<void>

End the client. Messages from it then fail with ClientClosed. Ending a closed client does nothing, also after its browser worker stopped.

end
();
}
return
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
Client.build(identity: PublicIdentity, options: ClientOptions, inboxId?: InboxId): Promise<Client>
build
(
identity: PublicIdentity
identity
, { ...
const options: ClientOptions
options
,
allowOffline?: boolean | undefined
allowOffline
: true },
const inboxId: string
inboxId
);
}

Use the new database’s actual OPFS entry name for dbPath and its attachment directory. Browser paths name OPFS entries; they are not host filesystem paths. The old default was xmtp-{env}-{inboxId}.db3. The new default uses the xmtp-sdk OPFS pool and a deployment/inbox layout. A default location does not move or convert an old database.

With default or directory storage, offline startup needs the saved backend deployment mapping and the saved inbox ID passed to build. Explicit paths do not need that path lookup. allowOffline: true permits startup from stored state. It does not prove that the backend used later is compatible.

Storage.admin() replaces Opfs.create(). It opens an independent lease on the package storage worker. Await admin.end() after use. There is no separate init() call.

Old API on main:

Before (main cc878025da)
import { Opfs } from "@xmtp/browser-sdk";
async function exportDatabase(path: string) {
const opfs = await Opfs.create();
try {
return await opfs.exportDb(path);
} finally {
opfs.close();
}
}

New storage administration:

export async function
function exportClosedDatabase(dbPath: string): Promise<Uint8Array<ArrayBufferLike>>
exportClosedDatabase
(
dbPath: string
dbPath
: string) {
// End every client that owns this database before an import or replacement.
const
const admin: StorageAdmin
admin
= await
class Storage
Storage
.
Storage.admin(): Promise<StorageAdmin>

Open a lease on the package storage worker. It needs no Client.

admin
();
try {
if (!(await
const admin: StorageAdmin
admin
.
StorageAdmin.fileExists(path: string): Promise<boolean>
fileExists
(
dbPath: string
dbPath
))) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("The database does not exist");
}
return await
const admin: StorageAdmin
admin
.
StorageAdmin.exportDb(path: string): Promise<Uint8Array>
exportDb
(
dbPath: string
dbPath
);
} finally {
await
const admin: StorageAdmin
admin
.
StorageAdmin.end(): Promise<void>
end
();
}
}
Old storage memberSelf-hosted member
new Opfs(), Opfs.create(), init()await Storage.admin(). Direct construction and initialization are removed.
close()await admin.end().
No client storage objectclient.storage.path() and client.storage.reconnect(). storage.delete() is excluded in Browser; use the admin lease for closed database files.

End each client that owns a database before import, restore, or replacement. Whole-database import rotates the delivery cursor identity. Archive import adds records and preserves that identity. Database export and archive export are different operations.

Conversation is now a TypeScript union of Group | Dm. It is no longer a base class that an app constructs. Narrow it with instanceof Group or the kind property. Group and DM methods remain on their objects.

Old API on main:

Before (main cc878025da)
import {
ConversationType,
IdentifierKind,
type Client,
} from "@xmtp/browser-sdk";
async function createConversations(client: Client, address: string) {
const peer = { identifier: address, identifierKind: IdentifierKind.Ethereum };
const dm = await client.conversations.createDmWithIdentifier(peer);
const group = await client.conversations.createGroupWithIdentifiers([peer], {
groupName: "Team",
});
const listed = await client.conversations.list({
conversationType: ConversationType.Group,
limit: 20n,
});
return { dm, group, listed };
}

New API:

export async function
function createConversations(client: Client, peer: PublicIdentity, extraMember: PublicIdentity): Promise<{
dm: Dm;
group: Group;
result: MembershipResult;
listed: Conversation[];
}>
createConversations
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
peer: PublicIdentity
peer
:
type PublicIdentity = {
readonly identifier: string;
readonly kind: PublicIdentityKind;
}
PublicIdentity
,
extraMember: PublicIdentity
extraMember
:
type PublicIdentity = {
readonly identifier: string;
readonly kind: PublicIdentityKind;
}
PublicIdentity
,
) {
const
const dm: Dm
dm
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.createDm(peer: InboxId | PublicIdentity, options?: CreateDmOptions): Promise<Dm>
createDm
(
peer: PublicIdentity
peer
);
const
const group: Group
group
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.createGroup(members: Array<InboxId> | Array<PublicIdentity>, options?: CreateGroupOptions): Promise<Group>
createGroup
([
peer: PublicIdentity
peer
], {
name?: string | undefined
name
: "Team",
permissions?: GroupPermissionMode | undefined
permissions
: {
kind: "adminOnly"
kind
: "adminOnly" },
});
const
const result: MembershipResult
result
= await
const group: Group
group
.
Group.addMembers(members: Array<InboxId> | Array<PublicIdentity>): Promise<MembershipResult>
addMembers
([
extraMember: PublicIdentity
extraMember
]);
const
const listed: Conversation[]
listed
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.list(options?: ListConversationsOptions): Promise<Array<Conversation>>
list
({
kind?: ConversationKind | undefined
kind
: "group",
consentStates?: ConsentState[] | undefined
consentStates
: ["allowed"],
orderBy?: ConversationOrder | undefined
orderBy
: "lastActivity",
limit?: number | undefined
limit
: 20,
});
return {
dm: Dm
dm
,
group: Group
group
,
result: MembershipResult
result
,
listed: Conversation[]
listed
};
}

Pass either inbox IDs or public identities to createGroup, createDm, addMembers, and removeMembers. A member list must use one form throughout. A mixed list fails with InvalidArgument before a call reaches the backend. An empty member list uses the inbox-ID route.

Old Conversations memberSelf-hosted member or action
getConversationById(id)getById(id). A missing conversation is undefined.
fetchDmByIdentifier(identifier)getDmByIdentity(identity).
createGroupWithIdentifiers(identifiers, options)createGroup(identities, options).
createDmWithIdentifier(identifier, options)createDm(identity, options).
createGroup(inboxIds, options), createDm(inboxId, options)Same names, with the inbox/identity union inputs.
createGroupOptimistic(options)Same name, with new group options.
list(options), listGroups(options), listDms(options)Same names. listGroups and listDms require the options position; pass undefined for defaults. Results are public conversation objects; use new query options.
syncAll(consentStates?)Call syncAll(consentStates) with an array or explicit undefined. It returns the new GroupSyncSummary.
topicRemoved from the collection. A conversation still has topic.
All stream* methodsUse ConversationStream, MessageStream, or client events. See the stream table below.
Old conversation optionSelf-hosted option
CreateGroupOptions.groupNamename.
groupImageUrlSquareimageUrl.
groupDescriptiondescription.
permissions, customPermissionPolicySetpermissions: { kind: "allMembers" }, { kind: "adminOnly" }, or { kind: "custom", policySet }.
messageDisappearingSettings: { fromNs, inNs }disappearing: { from: Timestamp, retentionNs: bigint }. This also applies to DM creation.
ListConversationsOptions.conversationTypekind: "group" | "dm".
createdBeforeNs, createdAfterNscreatedBefore, createdAfter, with Timestamp values.
limit as an i64/bigintlimit as a u32/number.
ListConversationsOrderBy enumorderBy: "createdAt" | "lastActivity".
consentStatesConsent values are "allowed", "denied", and "unknown".
No equivalentlastActivityBefore and lastActivityAfter, with Timestamp values.

The same required-position rule applies to lastActivityAt(contentTypes), userData(fields, inboxIds), and catchUpToLive(timeoutMs). Pass undefined for a default value instead of omitting that argument.

In PermissionPolicySet, rename addMemberPolicy, removeMemberPolicy, addAdminPolicy, and removeAdminPolicy to addMember, removeMember, addAdmin, and removeAdmin. Rename updateGroupNamePolicy, updateGroupDescriptionPolicy, updateGroupImageUrlSquarePolicy, updateMessageDisappearingPolicy, and updateAppDataPolicy to updateName, updateDescription, updateImage, updateDisappearing, and updateAppData. Use the new string policy values in each field.

The new API reads mutable values with await group.state() or await dm.state(). A group returns GroupState, with common state under common. A DM returns ConversationState directly. Read state again after an update when the app needs current values.

Old API on main:

Before (main cc878025da)
import type { Group } from "@xmtp/browser-sdk";
async function updateGroup(group: Group) {
const name = group.name;
const active = await group.isActive();
await group.updateAppData("updated");
await group.updateMessageDisappearingSettings(
1_000_000_000n,
86_400_000_000_000n,
);
await group.removeMessageDisappearingSettings();
return { name, active };
}

New API:

export async function
function readGroupState(group: Group): Promise<{
name: string;
admins: string[];
active: boolean;
}>
readGroupState
(
group: Group
group
:
class Group
Group
) {
const
const state: GroupState
state
= await
group: Group
group
.
Group.state(): Promise<GroupState>
state
();
await
group: Group
group
.
Group.updateAppData(value: string, expected: string | undefined): Promise<void>
updateAppData
("updated",
const state: GroupState
state
.
appData: string
appData
);
await
group: Group
group
.
Group.updateDisappearingSettings(settings: DisappearingSettings | undefined): Promise<void>
updateDisappearingSettings
({
from: Timestamp
from
: new
new Timestamp(ns: bigint): Timestamp
Timestamp
(
var BigInt: BigIntConstructor
(value: bigint | boolean | number | string) => bigint
BigInt
(
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
()) * 1_000_000n),
retentionNs: bigint
retentionNs
: 86_400_000_000_000n,
});
// Pass undefined to remove the disappearing settings.
await
group: Group
group
.
Group.updateDisappearingSettings(settings: DisappearingSettings | undefined): Promise<void>
updateDisappearingSettings
(
var undefined
undefined
);
return {
name: string
name
:
const state: GroupState
state
.
name: string
name
,
admins: string[]
admins
:
const state: GroupState
state
.
admins: string[]
admins
,
active: boolean
active
:
const state: GroupState
state
.
common: ConversationState
common
.
isActive: boolean
isActive
,
};
}
Old conversation or group memberSelf-hosted member or value
id, topic, addedByInboxIdReadonly properties. An unknown addedByInboxId is null.
createdAtNs, createdAtcreatedAt: Timestamp; read .ns or .date.
metadata.creatorInboxId, metadata.conversationTypecreatorInboxId and kind properties. An unknown creator is null. isCreator is also available. The old metadata record is removed.
isActive(), consentState(), pausedForVersion()Values in common state: isActive, consentState, pausedForVersion.
messageDisappearingSettings(), isMessageDisappearingEnabled()Common state values disappearingSettings and isDisappearingEnabled.
updateMessageDisappearingSettings(fromNs, inNs)updateDisappearingSettings({ from: new Timestamp(fromNs), retentionNs: inNs }).
removeMessageDisappearingSettings()updateDisappearingSettings(undefined).
updateConsentState(state)Same name, with string consent values.
name, imageUrl, description, appData, admins, superAdminsValues in await group.state().
permissions()(await group.state()).permissions. DM state has no permission-policy getter.
isPendingRemoval()(await group.state()).membershipState === "pendingRemove".
updateAppData(value)updateAppData(value, expected). Pass undefined for an unconditional update, or the last state value for a compare-and-set update.
updatePermission(type, policy, metadataField?)Use updatePermission(kind, policy, metadataField) with PermissionUpdateKind, new policy strings, and MetadataFieldKind. Pass undefined when no metadata field applies.
addMembersByIdentifiers, removeMembersByIdentifiersaddMembers(identities), removeMembers(identities).
removeMembers(inboxIds)removeMembers(inboxIdsOrIdentities). A member list must use one input form throughout.
addMembers(inboxIds)Same name. Returns MembershipResult with added, removed, and failedInstallationIds.
members()Same name; returns Member[], with identities instead of accountIdentifiers. The old installationIds field is absent.
lastMessage(), debugInfo()Same names; results use the new message and diagnostic types.
lastReadTimes()Same name; map values are Timestamp, not nanosecond integers.
processStreamedMessage(envelopeBytes)Removed. Use the SDK reader and message query APIs.
Dm.peerInboxId()Returns Promise<string | null> instead of Promise<string>. An unknown peer is null.

The standard send method names remain. All sends use SendOptions. The optimistic option remains available. prepareMessage() stores an unpublished message and queues optimistic publication. It does not keep an app draft private until a later publish call. idempotencyKey remains available on the new send options. Compression is optional and uses "deflate" or "gzip". If a send fails with PublishedButUnconfirmed, call conversation.sync() before deciding what to do next. Do not immediately send the content again.

Old API on main:

Before (main cc878025da)
import type { Conversation } from "@xmtp/browser-sdk";
async function sendMessages(conversation: Conversation) {
const id = await conversation.sendText("Hello");
await conversation.sendMarkdown("**Hello**");
await conversation.sendReadReceipt();
const pending = await conversation.sendText("Send later", {
optimistic: true,
});
await conversation.publishMessages();
return { id, pending };
}

New standard sends:

export async function
function sendMessages(conversation: Conversation): Promise<string>
sendMessages
(
conversation: Conversation
conversation
:
type Conversation = Group | Dm
Conversation
) {
const
const messageId: string
messageId
= await
conversation: Conversation
conversation
.
function sendText(text: string, options?: SendOptions): Promise<MessageId>
sendText
("Hello");
await
conversation: Conversation
conversation
.
function sendMarkdown(markdown: string, options?: SendOptions): Promise<MessageId>
sendMarkdown
("**Hello**", {
shouldPush?: boolean | undefined
shouldPush
: true });
await
conversation: Conversation
conversation
.
function sendReadReceipt(options?: SendOptions): Promise<MessageId>
sendReadReceipt
();
return
const messageId: string
messageId
;
}

New prepare and publish:

export async function
function prepareMessage(conversation: Conversation): Promise<string>
prepareMessage
(
conversation: Conversation
conversation
:
type Conversation = Group | Dm
Conversation
) {
await
function initPureWasm(wasm?: URL): Promise<void>
initPureWasm
();
const
const codec: TextCodec
codec
= new
new TextCodec(): TextCodec
TextCodec
();
const
const messageId: string
messageId
= await
conversation: Conversation
conversation
.
prepareMessage<string>(codec: ContentCodec<string>, value: string, options?: SendOptions): Promise<MessageId> (+1 overload)
prepareMessage
(
const codec: TextCodec
codec
, "Send later");
await
conversation: Conversation
conversation
.
function publishMessage(id: MessageId): Promise<void>
publishMessage
(
const messageId: string
messageId
);
return
const messageId: string
messageId
;
}
Old message operationSelf-hosted operation
send(encoded, SendMessageOpts)send(encoded, SendOptions) or send(codec, value, SendOptions).
sendText, sendMarkdown, sendReadReceiptSame names, with SendOptions.
sendAttachment, sendRemoteAttachment, sendMultiRemoteAttachmentSame names, with public content records and SendOptions.
sendTransactionReference, sendWalletSendCalls, sendActions, sendIntentSame names, with public content records and SendOptions.
sendReaction(reaction, options)sendReaction(reference, referenceInboxId, reaction, options?). The new Reaction contains action, schema, and content; target IDs are separate arguments.
sendReply(reply, options)sendReply(reference, referenceInboxId, encoded, options?). message.reply(text) is also available.
send*(..., { optimistic: true })Still supported in SendOptions. Alternatively, use prepareMessage(encoded) or prepareMessage(codec, value), then publishMessage(id) or publishMessages().
messages(options)Returns public Message[] with the new query options.
countMessages(options)Returns bigint. Pass undefined for the required options argument when no filter is needed.
No message actionsMessage.refresh(), delete(), deleteLocally(), react(), reply(), parent(), conversation(), and client(). Collection actions are also available.

Old API on main:

Before (main cc878025da)
import { SortDirection, type Conversation } from "@xmtp/browser-sdk";
async function history(conversation: Conversation) {
const first = await conversation.messages({
limit: 20n,
direction: SortDirection.Descending,
});
const last = first.at(-1);
return last
? conversation.messages({
limit: 20n,
direction: SortDirection.Descending,
sentBeforeNs: last.sentAtNs,
})
: [];
}

New timestamp query:

export async function
function readHistory(conversation: Conversation): Promise<{
firstPage: Message[];
nextPage: Message[];
}>
readHistory
(
conversation: Conversation
conversation
:
type Conversation = Group | Dm
Conversation
) {
const
const firstPage: Message[]
firstPage
= await
conversation: Conversation
conversation
.
function messages(options?: ListMessagesOptions): Promise<Array<Message>>
messages
({
limit?: number | undefined
limit
: 20,
sortBy?: MessageSortBy | undefined
sortBy
: "sentAt",
direction?: MessageOrder | undefined
direction
: "descending",
});
const
const last: Message | undefined
last
=
const firstPage: Message[]
firstPage
.
Array<Message>.at(index: number): Message | undefined

Returns the item located at the specified index.

@param ― index The zero-based index of the desired code unit. A negative index will count back from the last item.

at
(-1);
const
const nextPage: Message[]
nextPage
=
const last: Message | undefined
last
? await
conversation: Conversation
conversation
.
function messages(options?: ListMessagesOptions): Promise<Array<Message>>
messages
({
limit?: number | undefined
limit
: 20,
sortBy?: MessageSortBy | undefined
sortBy
: "sentAt",
direction?: MessageOrder | undefined
direction
: "descending",
sentBefore?: Timestamp | undefined
sentBefore
:
const last: Message
last
.
MessageFields.sentAt: Timestamp
sentAt
,
})
: [];
return {
firstPage: Message[]
firstPage
,
nextPage: Message[]
nextPage
};
}

Replace sentBeforeNs, sentAfterNs, insertedBeforeNs, and insertedAfterNs with sentBefore, sentAfter, insertedBefore, and insertedAfter. Use Timestamp values. Use number for a u32 limit. direction, sortBy, deliveryStatus, and message kind use string values. contentTypes and excludeContentTypes take ContentTypeId[]. The old ContentType.Custom wildcard is removed. Keep excludeSenderInboxIds when needed.

A sent-time cutoff is not a unique pagination cursor. Messages can have the same timestamp. Use a history snapshot and its delivery cursor for a history/live boundary.

DecodedMessage<ContentTypes> is replaced by Message. Its content is a tagged union. Check message.content.kind before reading a variant. The SDK does not use the old generic content union or the exported isText, isReply, and related guard helpers.

Old API on main:

Before (main cc878025da)
import { isText, type DecodedMessage } from "@xmtp/browser-sdk";
function display(message: DecodedMessage) {
return isText(message) ? (message.content ?? "") : (message.fallback ?? "");
}

New content handling:

export function
function displayMessage(message: Message): string
displayMessage
(
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 {
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":
case "markdown":
return
message: Message
message
.
Message.content: {
readonly kind: "text";
readonly value: string;
} | {
readonly kind: "markdown";
readonly value: string;
}
content
.
value: string
value
;
case "custom":
return
message: Message
message
.
MessageFields.fallback?: string | undefined
fallback
?? "Custom content";
case "unknown":
return
message: Message
message
.
MessageFields.fallback?: string | undefined
fallback
?? "Cannot decode this content";
default:
return
message: Message
message
.
MessageFields.fallback?: string | undefined
fallback
??
message: Message
message
.
Message.content: {
readonly kind: "readReceipt";
} | {
readonly kind: "reaction";
readonly reference: MessageId;
readonly referenceInboxId?: InboxId | undefined;
readonly reaction: Reaction;
} | {
readonly kind: "attachment";
readonly value: Attachment;
} | {
readonly kind: "remoteAttachment";
readonly value: RemoteAttachment;
} | {
readonly kind: "multiRemoteAttachment";
readonly value: MultiRemoteAttachment;
} | ... 7 more ... | {
...;
}
content
.
kind: "readReceipt" | "reaction" | "attachment" | "remoteAttachment" | "multiRemoteAttachment" | "transactionReference" | "walletSendCalls" | "actions" | "intent" | "groupUpdated" | "leaveRequest" | "deletedMessage" | "reply"
kind
;
}
}
Old DecodedMessage fieldSelf-hosted Message field
sentAt: Date, sentAtNs: bigintsentAt: Timestamp; use .date or .ns.
expiresAt, expiresAtNsOptional expiresAt: Timestamp. insertedAt is also available.
kind, deliveryStatus numeric enumsString union values.
contentType always presentOptional contentType. A malformed envelope can have no type.
content: ContentTypes | undefinedMessageContent, with kind and variant fields. Custom content has optional value and error. Unknown content has typed error.
numRepliesreplyCount: bigint.
reactions: DecodedMessage<Reaction>[]ReactionMessage[], with IDs, sender IDs, Timestamp, and reaction.
Reply content.referenceId, content.content, content.inReplyToReply content has referenceId and body. The message has optional inReplyTo, inReplyToContent, and replyContent. parent() fetches the full parent.
No equivalentExact rawBytes, optional encoded, topic, and deliveryCursor.

Unknown or failed content does not stop delivery of later messages. Preserve rawBytes. Do not replace absent encoded content or a missing type with invented empty values. CodecNotFound, CodecDecodeFailed, and MalformedEnvelope are separate failure codes.

New reply and reaction actions:

export async function
function actOnMessage(message: Message): Promise<{
replyId: string;
reactionId: string;
parent: Message | undefined;
conversation: Conversation | undefined;
}>
actOnMessage
(
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
) {
const
const replyId: string
replyId
= await
message: Message
message
.
Message.reply(content: string | EncodedContent, options?: SendOptions): Promise<MessageId> (+1 overload)
reply
("Received");
const
const reactionId: string
reactionId
= await
message: Message
message
.
Message.react(reaction: Reaction, options?: SendOptions): Promise<MessageId>
react
({
action: ReactionAction
action
: "added",
schema: ReactionSchema
schema
: "unicode",
content: string
content
: "👍",
});
const
const parent: Message | undefined
parent
= await
message: Message
message
.
Message.parent(): Promise<Message | undefined>
parent
();
const
const conversation: Conversation | undefined
conversation
= await
message: Message
message
.
Message.conversation(): Promise<Conversation | undefined>
conversation
();
return {
replyId: string
replyId
,
reactionId: string
reactionId
,
parent: Message | undefined
parent
,
conversation: Conversation | undefined
conversation
};
}
export async function
function deleteMessage(message: Message): Promise<string>
deleteMessage
(
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
) {
return
message: Message
message
.
Message.delete(): Promise<MessageId>
delete
();
}

message.delete() publishes a typed deletion action and returns its message ID. message.deleteLocally() removes only the local record. DeleteMessageCodec remains available in the pure entry for a typed delete content value. The old deleted-message stream is replaced by client events.

Replace streams and save delivery progress

Section titled “Replace streams and save delivery progress”

Use MessageStream for durable message delivery. Use ConversationStream for joined or created conversations. Both implement asynchronous iteration. They open without an awaited static factory call. Client events use await client.events(filter).

Old API on main:

Before (main cc878025da)
import type { Client } from "@xmtp/browser-sdk";
async function receive(client: Client) {
const stream = await client.conversations.streamAllMessages();
try {
for await (const message of stream) console.log(message.id);
} finally {
await stream.end();
}
}

New durable delivery:

export async function
function receiveMessages(client: Client, handle: (message: Message) => Promise<void>, signal: AbortSignal): Promise<void>
receiveMessages
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
handle: (message: Message) => Promise<void>
handle
: (
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
) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void>,
signal: AbortSignal
signal
:
interface AbortSignal

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
,
) {
const
const stream: MessageStream
stream
=
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.streamAllMessages(options?: MessageStreamOptions): MessageStream
streamAllMessages
({
consentStates?: readonly ConsentState[] | undefined
consentStates
: ["allowed", "unknown"],
signal?: AbortSignal | undefined
signal
,
});
try {
for await (const
const message: Message
message
of
const stream: MessageStream
stream
) {
await
handle: (message: Message) => Promise<void>
handle
(
const message: Message
message
);
}
} finally {
await
const stream: MessageStream
stream
.
ReaderStream<Message>.end(): Promise<void>
end
();
}
}
Old stream callSelf-hosted call
conversation.stream()group.streamMessages() or dm.streamMessages().
conversations.streamAllMessages()conversations.streamAllMessages(options?).
streamAllGroupMessages()conversations.streamAllMessages({ conversationKind: "group" }).
streamAllDmMessages()conversations.streamAllMessages({ conversationKind: "dm" }).
conversations.stream()conversations.stream(options?).
streamGroups(), streamDms()conversations.stream({ conversationKind: "group" }) or { conversationKind: "dm" }.
streamMessageDeletions(), streamDeletedMessages()client.events({ kinds: ["message.deleted"] }). Read the typed event payload.
preferences.streamConsent()client.events({ kinds: ["consent.changed"] }).
preferences.streamPreferences()Client events for the required changes, such as hmac_keys.updated. There is no UserPreferenceUpdate[] stream.
AsyncStreamProxy.next(), return(), end()Use for await and await stream.end(). return() also closes.
StreamOptions.onValuestream.onValue(async message => { ... }) as a separate consumer mode. Do not also iterate that stream.
onEnd, onError, onFail, onRestart, onRetryonClose(reason) and onConnectionStateChange(previous, current). A failed close includes the error.
retryAttempts, retryDelay, retryOnFail, disableSyncRemoved from the generated stream options. Core handles retryable message-stream network faults. Call sync() explicitly when a snapshot needs a sync.
No equivalentsignal: AbortSignal for cancellation; from: DeliveryCursor for replay.

The all-conversation message reader defaults to allowed and unknown consent. An explicit consent list replaces that default. An empty list selects no messages. A named group or DM reader has no consent filter. The selection remains fixed for the life of a reader.

One default message reader owns delivery progress for a database. Another default reader fails with ConsumerOwned, even when it selects a different group. A reader with from is an independent replay/live reader. It does not change default delivery progress.

The next iterator request acknowledges the previous message. Await app work before the next request. A successful onValue callback return acknowledges the delivered message. Loop exit, cancellation, return(), and end() do not acknowledge the last yielded message. Save a cursor only after successful app work. Deduplicate repeat work by message ID when needed.

Use messageHistorySnapshot(limit, selection) when a history query and live delivery need one boundary. The example reads up to 100 retained messages, then starts replay strictly after the snapshot cursor. Choose the history limit for the app. This is a local retained-history snapshot; it is not a request for all historical backend messages.

export async function
function receiveHistoryAndLive(client: Client, handle: (message: Message) => Promise<void>, saveCursor: (cursor: DeliveryCursor) => Promise<void>, signal: AbortSignal): Promise<void>
receiveHistoryAndLive
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
handle: (message: Message) => Promise<void>
handle
: (
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
) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void>,
saveCursor: (cursor: DeliveryCursor) => Promise<void>
saveCursor
: (
cursor: string
cursor
:
type DeliveryCursor = string

An opaque replay position in one database.

DeliveryCursor
) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void>,
signal: AbortSignal
signal
:
interface AbortSignal

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
,
) {
const
const selection: {
consentStates: "allowed"[];
}
selection
= {
consentStates: "allowed"[]
consentStates
: ["allowed" as
type const = "allowed"
const
] };
const
const snapshot: MessageHistorySnapshot
snapshot
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.messageHistorySnapshot(limit: number, options?: MessageReaderOptions): Promise<MessageHistorySnapshot>
messageHistorySnapshot
(
100,
const selection: {
consentStates: "allowed"[];
}
selection
,
);
for (const
const message: Message
message
of
const snapshot: MessageHistorySnapshot
snapshot
.
messages: Message[]
messages
) await
handle: (message: Message) => Promise<void>
handle
(
const message: Message
message
);
await
saveCursor: (cursor: DeliveryCursor) => Promise<void>
saveCursor
(
const snapshot: MessageHistorySnapshot
snapshot
.
cursor: string
cursor
);
const
const stream: MessageStream
stream
=
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.streamAllMessages(options?: MessageStreamOptions): MessageStream
streamAllMessages
({
...
const selection: {
consentStates: "allowed"[];
}
selection
,
from?: string | undefined
from
:
const snapshot: MessageHistorySnapshot
snapshot
.
cursor: string
cursor
,
signal?: AbortSignal | undefined
signal
,
});
try {
for await (const
const message: Message
message
of
const stream: MessageStream
stream
) {
await
handle: (message: Message) => Promise<void>
handle
(
const message: Message
message
);
if (
const message: Message
message
.
MessageFields.deliveryCursor: string | null

The committed delivery position, or null when there is none.

deliveryCursor
!== null) {
await
saveCursor: (cursor: DeliveryCursor) => Promise<void>
saveCursor
(
const message: Message
message
.
MessageFields.deliveryCursor: string

The committed delivery position, or null when there is none.

deliveryCursor
);
}
}
} finally {
await
const stream: MessageStream
stream
.
ReaderStream<Message>.end(): Promise<void>
end
();
}
}

Keep a cursor unchanged. A cursor belongs to one database. Treat InvalidCursor, ForeignCursor, and ConsumerOwned as separate errors. Use beginningDeliveryCursor() when replay must start at the first retained delivery.

New conversation stream:

export async function
function receiveConversations(client: Client, handle: (conversation: Conversation) => Promise<void>, signal: AbortSignal): Promise<void>
receiveConversations
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
handle: (conversation: Conversation) => Promise<void>
handle
: (
conversation: Conversation
conversation
:
type Conversation = Group | Dm
Conversation
) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void>,
signal: AbortSignal
signal
:
interface AbortSignal

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
,
) {
const
const stream: ConversationStream
stream
=
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.stream(options?: ConversationStreamOptions): ConversationStream
stream
({
conversationKind?: ConversationKind | undefined
conversationKind
: "group",
signal?: AbortSignal | undefined
signal
,
});
try {
for await (const
const conversation: Conversation
conversation
of
const stream: ConversationStream
stream
) await
handle: (conversation: Conversation) => Promise<void>
handle
(
const conversation: Conversation
conversation
);
} finally {
await
const stream: ConversationStream
stream
.
ReaderStream<Conversation>.end(): Promise<void>
end
();
}
}

Old consent records used entityType, entity, and numeric enum values. New consent records use { entity: { kind, ... }, state }. A consent entity is an inbox or a conversation.

Old API on main:

Before (main cc878025da)
import {
ConsentEntityType,
ConsentState,
type Client,
} from "@xmtp/browser-sdk";
async function allow(client: Client, conversationId: string) {
await client.preferences.setConsentStates([
{
entityType: ConsentEntityType.ConversationId,
entity: conversationId,
state: ConsentState.Allowed,
},
]);
return client.preferences.getConsentState(
ConsentEntityType.ConversationId,
conversationId,
);
}

New consent and events:

export async function
function allowConversation(client: Client, conversationId: string): Promise<ConsentState>
allowConversation
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
conversationId: string
conversationId
: string,
) {
const
const entity: {
kind: "conversation";
conversationId: string;
}
entity
= {
kind: "conversation"
kind
: "conversation" as
type const = "conversation"
const
,
conversationId: string
conversationId
};
await
client: Client
client
.
ClientMembers.preferences: Preferences
preferences
.
Preferences.setConsentStates(records: Array<ConsentRecord>): Promise<void>
setConsentStates
([{
entity: ConsentEntity
entity
,
state: ConsentState
state
: "allowed" }]);
return
client: Client
client
.
ClientMembers.preferences: Preferences
preferences
.
Preferences.consentState(entity: ConsentEntity): Promise<ConsentState>
consentState
(
const entity: {
kind: "conversation";
conversationId: string;
}
entity
);
}
export async function
function consentEvents(client: Client): Promise<EventStream>
consentEvents
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
) {
return
client: Client
client
.
Client.events(filter: EventFilter): Promise<EventStream>

A stream of the client events that filter selects.

events
({
kinds: EventKind[]
kinds
: ["consent.changed"],
references_own_messages?: boolean | undefined
references_own_messages
: false,
});
}
Old client or preference memberSelf-hosted member
preferences.setConsentStates(records)Same names; use new records.
preferences.getConsentState(type, entity)preferences.consentState(entity).
preferences.inboxState(), fetchInboxState()client.inboxState(false) or client.inboxState(true). The boolean selects network refresh.
preferences.getInboxStates(ids), fetchInboxStates(ids)client.inboxStates(ids, false) or client.inboxStates(ids, true).
Static Client.fetchInboxStates(ids, backendOrEnv?, gatewayHost?)Client.inboxStates(ids, backend).
fetchInboxIdByIdentifier(identifier)client.inboxIdFor(identity).
getInboxIdForIdentifier(backend, identifier)Client.inboxIdFor(identity, backend). It resolves an association or derives the nonce-zero inbox ID when none is registered. Use canMessage to check registration.
syncAllDeviceSyncGroups()GroupSyncSummary now has eligible and synced fields instead of numEligible and numSynced.
canMessage(identifiers), static Client.canMessage(...)Same method name with PublicIdentity[]. The static form requires a backend. Keys include ethereum:, passkey:, or handle:.
fetchLatestInboxUpdatesCount(ids)client.latestInboxUpdatesCount(ids, refreshFromNetwork). Pass the required boolean to select a network refresh.
Static Client.fetchLatestInboxUpdatesCount(...)Root latestInboxUpdatesCount(ids, backend).
fetchOwnInboxUpdatesCount()client.ownInboxUpdatesCount(refreshFromNetwork). Pass the required boolean. Counts now use bigint.
fetchKeyPackageStatuses(ids)client.keyPackageStatuses(ids). The static Client.keyPackageStatuses(ids, backend) is also available.
accountIdentifieridentity: PublicIdentity.
inboxId, installationId, installationIdBytes, libxmtpVersion, appVersion, conversations, preferences, optionsReadonly properties. appVersion can be absent. options omits credential secrets.
signer, isReady, envRemoved. Retain the app signer when it is needed. Await client creation/build before use.
Public constructor, init(identifier)Removed. Use Client.create or Client.build.
close()await client.end().
Instance verifySignedWithPublicKey(text, signature, publicKey)Static Client.verifySignedWithPublicKey(text, signature, publicKey).

Handles are new. An inbox can claim a public name such as nick#2341, and a PublicIdentity of kind handle finds that inbox. Static Client.inboxIdFor never derives an inbox ID from a handle: it fails with IdentityNotFound for a handle that no inbox claimed. See Handles.

New identity queries and signature request:

export async function
function identityQueries(client: Client, identity: PublicIdentity, backend: BackendSource): Promise<{
inboxId: string;
canMessage: Map<string, boolean>;
ownState: InboxState;
states: InboxState[];
}>
identityQueries
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
identity: PublicIdentity
identity
:
type PublicIdentity = {
readonly identifier: string;
readonly kind: PublicIdentityKind;
}
PublicIdentity
,
backend: BackendSource
backend
:
type BackendSource = BackendOptions | Backend
BackendSource
,
) {
const
const inboxId: string
inboxId
= await
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
ClientMembers.inboxIdFor(identity: PublicIdentity, backend: BackendSource): Promise<InboxId>
inboxIdFor
(
identity: PublicIdentity
identity
,
backend: BackendSource
backend
);
const
const canMessage: Map<string, boolean>
canMessage
= await
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
ClientMembers.canMessage(identities: Array<PublicIdentity>, backend: BackendSource): Promise<Map<string, boolean>>
canMessage
([
identity: PublicIdentity
identity
],
backend: BackendSource
backend
);
const
const ownState: InboxState
ownState
= await
client: Client
client
.
ClientMembers.inboxState(refreshFromNetwork: boolean): Promise<InboxState>
inboxState
(true);
const
const states: InboxState[]
states
= await
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
ClientMembers.inboxStates(ids: Array<InboxId>, backend: BackendSource): Promise<Array<InboxState>>
inboxStates
([
const inboxId: string
inboxId
],
backend: BackendSource
backend
);
return {
inboxId: string
inboxId
,
canMessage: Map<string, boolean>
canMessage
,
ownState: InboxState
ownState
,
states: InboxState[]
states
};
}
export async function
function signAccountRequest(client: Client, identity: PublicIdentity, signer: Signer): Promise<string>
signAccountRequest
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
identity: PublicIdentity
identity
:
type PublicIdentity = {
readonly identifier: string;
readonly kind: PublicIdentityKind;
}
PublicIdentity
,
signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
,
) {
const
const request: SignatureRequest
request
= await
client: Client
client
.
ClientMembers.unsafeRemoveAccountSignatureRequest(identity: PublicIdentity): Promise<SignatureRequest>
unsafeRemoveAccountSignatureRequest
(
identity: PublicIdentity
identity
);
const
const text: string
text
= await
const request: SignatureRequest
request
.
SignatureRequest.signatureText(): Promise<string>
signatureText
();
await
const request: SignatureRequest
request
.
SignatureRequest.sign(signer: Signer): Promise<void>
sign
(
signer: Signer
signer
);
await
client: Client
client
.
ClientMembers.unsafeApplySignatureRequest(request: SignatureRequest): Promise<void>
unsafeApplySignatureRequest
(
const request: SignatureRequest
request
);
return
const text: string
text
;
}

The new signature workflow returns SignatureRequest objects. It does not return a text/request-ID pair. Read signatureText(), call sign(signer) or addSignature(signature), then apply the request. An optional request can be undefined when no update is needed.

Old account methodSelf-hosted method
unsafe_createInboxSignatureText()unsafeCreateInboxSignatureRequest().
unsafe_addAccountSignatureText(identity, allowInboxReassign)unsafeAddAccountSignatureRequest(identity, allowInboxReassign).
unsafe_removeAccountSignatureText(identity)unsafeRemoveAccountSignatureRequest(identity).
unsafe_revokeInstallationsSignatureText(byteIds)unsafeRevokeInstallationsSignatureRequest(stringIds).
unsafe_revokeAllOtherInstallationsSignatureText()unsafeRevokeAllOtherInstallationsSignatureRequest().
unsafe_changeRecoveryIdentifierSignatureText(identity)unsafeChangeRecoveryIdentifierSignatureRequest(identity).
unsafe_applySignatureRequest(safeSigner, requestId)unsafeApplySignatureRequest(request), after signing the request.
unsafe_addAccount(signer, allowInboxReassign)unsafeAddAccount(signer, allowInboxReassign).
removeAccount(identity)removeAccount(recoverySigner, identity).
revokeInstallations(byteIds)revokeInstallations(signer, stringIds).
revokeAllOtherInstallations()revokeAllOtherInstallations(signer).
changeRecoveryIdentifier(identity)changeRecoveryIdentifier(signer, identity).
Static Client.revokeInstallations(signer, inboxId, byteIds, backendOrEnv?, gatewayHost?)Static Client.revokeInstallations(signer, inboxId, stringIds, backend).

InstallationId is a validated hex string. Keep installationIdBytes only when an operation explicitly needs bytes. InboxState.identities and its recovery identity use PublicIdentity. Installation timestamps use Timestamp, and sequence/count values use bigint.

Old API on main:

Before (main cc878025da)
import type { Client } from "@xmtp/browser-sdk";
async function backup(client: Client, key: Uint8Array) {
const bytes = await client.createArchive(key);
const metadata = await client.archiveMetadata(bytes, key);
await client.importArchive(bytes, key);
return metadata;
}

New archive export and import:

export async function
function exportArchive(client: Client, key: Uint8Array): Promise<Uint8Array<ArrayBufferLike>>
exportArchive
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
key: Uint8Array<ArrayBufferLike>
key
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
) {
return
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.exportToBytes(keyBytes: Uint8Array, options: ArchiveOptions | undefined): Promise<Uint8Array>
exportToBytes
(
key: Uint8Array<ArrayBufferLike>
key
, {
elements?: ArchiveElement[] | undefined
elements
: ["messages", "consent"],
excludeDisappearingMessages?: boolean | undefined
excludeDisappearingMessages
: true,
});
}
export async function
function importArchive(client: Client, data: Uint8Array, key: Uint8Array): Promise<ArchiveMetadata>
importArchive
(
client: Client
client
:
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
,
data: Uint8Array<ArrayBufferLike>
data
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
,
key: Uint8Array<ArrayBufferLike>
key
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
,
) {
const
const metadata: ArchiveMetadata
metadata
= await
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.metadataFromBytes(data: Uint8Array, keyBytes: Uint8Array): Promise<ArchiveMetadata>
metadataFromBytes
(
data: Uint8Array<ArrayBufferLike>
data
,
key: Uint8Array<ArrayBufferLike>
key
);
await
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.importFromBytes(data: Uint8Array, keyBytes: Uint8Array): Promise<void>
importFromBytes
(
data: Uint8Array<ArrayBufferLike>
data
,
key: Uint8Array<ArrayBufferLike>
key
);
return
const metadata: ArchiveMetadata
metadata
;
}
Old memberSelf-hosted member or action
client.createArchive(key, options)client.archives.exportToBytes(key, options). Pass undefined for the required options argument when using defaults.
client.importArchive(bytes, key)client.archives.importFromBytes(bytes, key).
client.archiveMetadata(bytes, key)client.archives.metadataFromBytes(bytes, key).
ArchiveOptions.startNs, endNsstart, end, with Timestamp values.
BackupElementSelectionOptionelements: ["messages", "consent"] or a chosen subset.
ArchiveMetadata.exportedAtNs, startNs, endNsexportedAt, start, end, with Timestamp values.
sendSyncRequest, sendSyncArchive, processSyncArchive, listAvailableArchivesRemoved. There is no replacement API for the old archive-server and PIN workflow. Keep ordinary deviceSync and syncAllDeviceSyncGroups separate from archive import/export.
debugInformation.apiStatistics()diagnostics.apiStatistics().
debugInformation.apiIdentityStatistics()diagnostics.identityStatistics().
debugInformation.apiAggregateStatistics()diagnostics.aggregateStatistics().
debugInformation.clearAllStatistics()diagnostics.clearStatistics().

Browser archive APIs use bytes. Native filesystem archive methods are excluded. Use a 32-byte archive encryption key. Store the key separately from the archive. Archive options default to messages and consent when elements is absent.

The new package exports string unions for enums. Use values such as "allowed", "descending", "published", "application", "admin", and "superAdmin". Do not keep numeric enum values from the old bindings.

Old public type or resultSelf-hosted type or result
ConversationType, GroupMessageKindConversationKind and MessageKind.
SortDirection, ListConversationsOrderByMessageOrder and ConversationOrder.
GroupMembershipState, GroupPermissionsOptionsMembershipState and GroupPermissionMode. State permissions use GroupPolicyType.
Identifier, IdentifierKindPublicIdentity, PublicIdentityKind.
GroupMemberMember; account identities are in identities.
MessageDisappearingSettingsDisappearingSettings.
SendOpts, SendMessageOptsSendOptions.
Consent, ConsentEntityTypeConsentRecord and tagged ConsentEntity.
PermissionUpdateType, MetadataFieldPermissionUpdateKind, MetadataFieldKind.
BackupElementSelectionOption, LifetimeArchiveElement, KeyPackageLifetime.
InboxState.accountIdentifiers, recoveryIdentifieridentities, recoveryIdentity. creationSignatureKind is also available.
Installation.bytes, clientTimestampNsInstallation.id identifies the installation; optional createdAt is a Timestamp. There is no installation bytes field.
GroupSyncSummary.numEligible, numSyncedeligible, synced, both bigint.
GroupUpdated inbox arrays of { inboxId }Inbox-ID string arrays with the same field names.
ConversationDebugInfo.cursor array of { originatorId, sequenceId }Array of bigint cursor values.
ApiStats per-operation counterspublish, query, queryNewest, subscribe, and subscribeStatic, all bigint.
IdentityStats.publishIdentityUpdate, getInboxIds, verifySmartContractWalletSignaturegetInboxIds and verifySmartContractWalletSignatures, both bigint. The old publish counter is absent.
LogOptionsLoggingOptions, passed to initLogging().
WorkerConfigOptions, WorkerIntervalOverrideWorkerOptions, with WorkerInterval records. Each record can set intervalNs, jitterNs, and enabled.
Intent.metadata objectmetadataJson?: string; serialize and validate the JSON explicitly.
Actions.expiresAtNs, Action.expiresAtNsOptional expiresAt: Timestamp.
WalletCall.metadata objectWalletCallMetadata with description, transactionType, and extra: Map<string, string>. WalletSendCalls.capabilities is also a Map.
DeletedMessage.deletedBy enum and separate adminInboxIdTagged deletedBy: { kind: "sender" } or { kind: "admin", inboxId }.

ActionStyle, ConsentState, DeliveryStatus, PermissionLevel, PermissionPolicy, ReactionAction, ReactionSchema, LogLevel, and WorkerKind remain type names with string values. The new worker kinds include configuration refresh, HMAC epochs, and attachment cleanup. Check the new type when a switch must cover every variant.

The old encode* and contentType* root helpers initialized WASM and returned promises. Use standalone standard codecs in /pure. Call await initPureWasm() before constructing or using a standalone standard codec. Encoding and decoding are synchronous after initialization.

Old export familySelf-hosted form
encodeText, contentTypeTextTextCodec.encode(value) and TextCodec.type.
encodeMarkdown, contentTypeMarkdownMarkdownCodec.
encodeReadReceipt, contentTypeReadReceiptReadReceiptCodec.
encodeReaction, contentTypeReactionReactionV2Codec; its typed value carries the target ID and the reaction. Legacy reaction v1 needs an app codec.
encodeAttachment, contentTypeAttachmentAttachmentCodec.
encodeRemoteAttachment, contentTypeRemoteAttachmentRemoteAttachmentCodec.
encodeMultiRemoteAttachment, contentTypeMultiRemoteAttachmentMultiRemoteAttachmentCodec.
encodeTransactionReference, contentTypeTransactionReferenceTransactionReferenceCodec.
encodeWalletSendCalls, contentTypeWalletSendCallsWalletSendCallsCodec.
encodeActions, contentTypeActionsActionsCodec.
encodeIntent, contentTypeIntentIntentCodec.
contentTypeReply, contentTypeGroupUpdated, contentTypeLeaveRequestReplyCodec, GroupUpdatedCodec, and LeaveRequestCodec.
encryptAttachment, decryptAttachmentPublic encryptEncodedContent and decryptEncodedContent for encrypted content, or client.attachments for managed transfers. The old attachment result shape does not apply.
generateInboxId(identifier, nonce?)Pure generateInboxId(identity, nonce?), after initPureWasm(). Nonces remain bigint; the default changes from 1n to 0n. Pass 1n explicitly to preserve the old derivation.
metadataFieldName, MetadataFieldMetadataFieldKind for permission updates; metadata descriptors and field APIs for app data.
SafeConversation, toSafeConversationRemoved. Public Group and Dm are the supported objects.
HmacKeys, LastReadTimesUse the method result types. HMAC keys are maps/lists of HmacKey; read-time values are Timestamp.
isText, isMarkdown, isReply, isTextReply, isReaction, isAttachment, isRemoteAttachment, isMultiRemoteAttachment, isTransactionReference, isGroupUpdated, isReadReceipt, isLeaveRequest, isWalletSendCalls, isIntent, isActionsNarrow message.content.kind, then read that variant.
ApiUrls, HistorySyncUrls, XmtpEnv, NetworkOptions, DeviceSyncOptions, OtherOptions, ContentOptionsRemoved legacy configuration exports. Use BackendOptions and ClientOptions.
EOASigner, SCWSigner, SafeSigner, BuiltInContentTypes, ExtractCodecContentTypes, EnrichedReplyRemoved. Use Signer, PublicIdentity, MessageContent, and MessageBody.

Replace the old codec contentType property with type. A custom codec now exposes type, synchronous encode, synchronous decode, and optional fallback/shouldPush hooks. Import ContentCodec, EncodedContent, and ContentTypeId from the Browser SDK root. EncodedContent.type is required. parameters changes from Record<string, string> to Map<string, string>. The old envelope compression field is removed; use SendOptions.compression. Register codecs in ClientOptions.codecs. Use the typed conversation.send(codec, value) overload to send a custom value.

Old API on main:

Before (main cc878025da)
import type { ContentCodec } from "@xmtp/content-type-primitives";
import type { EncodedContent } from "@xmtp/wasm-bindings";
import type { Conversation } from "@xmtp/browser-sdk";
const noteCodec: ContentCodec<string> = {
contentType: {
authorityId: "example.com",
typeId: "note",
versionMajor: 1,
versionMinor: 0,
},
encode(value: string): EncodedContent {
return {
type: this.contentType,
parameters: {},
content: new TextEncoder().encode(value),
};
},
decode(encoded: EncodedContent) {
return new TextDecoder().decode(encoded.content);
},
fallback: (value) => value,
shouldPush: () => true,
};
async function sendNote(conversation: Conversation) {
return conversation.send(noteCodec.encode("A typed note"));
}

New API:

export const
const noteCodec: ContentCodec<string>
noteCodec
:
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> = {
ContentCodec<string>.type: ContentTypeId
type
: {
authorityId: string
authorityId
: "example.com",
typeId: string
typeId
: "note",
versionMajor: number
versionMajor
: 1,
versionMinor: number
versionMinor
: 0,
},
ContentCodec<string>.encode: (value: string) => EncodedContent
encode
: (
value: string
value
):
type EncodedContent = {
readonly type: ContentTypeId;
readonly parameters?: Map<string, string>;
readonly fallback?: string | undefined;
readonly content: Uint8Array;
}
EncodedContent
=> ({
type: ContentTypeId
type
:
const noteCodec: ContentCodec<string>
noteCodec
.
ContentCodec<string>.type: ContentTypeId
type
,
parameters?: Map<string, string> | undefined
parameters
: new
var Map: MapConstructor
new <string, string>(iterable?: Iterable<readonly [string, string]> | null | undefined) => Map<string, string> (+3 overloads)
Map
<string, string>(),
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
(
value: string
value
),
}),
ContentCodec<string>.decode: (encoded: EncodedContent) => string
decode
: (
encoded: EncodedContent
encoded
) => 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
(
encoded: EncodedContent
encoded
.
content: Uint8Array<ArrayBufferLike>
content
),
ContentCodec<string>.fallback?: ((value: string) => string | undefined) | undefined

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

fallback
: (
value: string
value
) =>
value: string
value
,
ContentCodec<string>.shouldPush?: ((value: string) => boolean) | undefined

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

shouldPush
: () => true,
};
export async function
function createWithCodec(signer: Signer): Promise<Client>
createWithCodec
(
signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
) {
return
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
Client.create(signer: Signer, options: ClientOptions): Promise<Client>
create
(
signer: Signer
signer
, {
backend?: BackendSource | undefined
backend
: {
url: string
url
: "https://xmtp.example.com" },
storage: StorageOptions
storage
: {
location: StorageLocation
location
: "default" },
codecs?: readonly AnyContentCodec[] | undefined
codecs
: [
const noteCodec: ContentCodec<string>
noteCodec
],
});
}
export async function
function sendCustom(conversation: Conversation): Promise<string>
sendCustom
(
conversation: Conversation
conversation
:
type Conversation = Group | Dm
Conversation
) {
return
conversation: Conversation
conversation
.
send<string>(codec: ContentCodec<string>, value: string, options?: SendOptions): Promise<MessageId> (+1 overload)
send
(
const noteCodec: ContentCodec<string>
noteCodec
, "A typed note");
}

Catalogue content types have defined push defaults. Read receipts, reactions, group updates, deletion, and leave requests default to no push. An explicit SendOptions.shouldPush wins. A custom codec’s hook does not replace the default for a catalogue type.

An encrypted-content helper result uses { ciphertext, keys }. The keys record has secret, salt, nonce, digest, and length. encryptEncodedContent accepts serialized envelope bytes. decryptEncodedContent returns serialized envelope bytes. These are not the old attachment helper records.

The new attachment API adds client.attachments for upload, download, status, and deletion. Browser path sources refer to OPFS. Byte sources copy the caller’s bytes at the Browser boundary. Transfer failures preserve a typed attachment cause; the SDK does not add automatic transfer retries.

The new API also adds backend configuration reads, credentials, catch-up state, metadata field descriptors and values, user data, client events, and message actions. These have no direct old Browser method to rename.

The old exported error classes (ClientNotInitializedError, SignerUnavailableError, InboxReassignError, AccountAlreadyAssociatedError, GroupNotFoundError, StreamNotFoundError, StreamFailedError, StreamInvalidRetryAttemptsError, OpfsNotInitializedError, and OpfsInitializationError) are removed. Catch the new XmtpError base or a public typed subclass. Read details.code, category, retryable, and message. Do not classify a failure by display text.

export function
function describeError(error: unknown): string
describeError
(
error: unknown
error
: unknown): string {
if (
error: unknown
error
instanceof
class XmtpError

A failure from the XMTP SDK. Its subclass names the failure variant. details.code keeps the Rust cause code.

XmtpError
) {
const {
const code: string
code
,
const retryable: boolean
retryable
,
const message: string
message
} =
error: XmtpError
error
.
XmtpError.details: ErrorDetails
details
;
return `${
const code: string
code
}: ${
const message: string
message
}; retryable=${
const retryable: boolean
retryable
}`;
}
return
error: unknown
error
instanceof
var Error: ErrorConstructor
Error
?
error: Error
error
.
Error.message: string
message
:
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
error: unknown
error
);
}

Keep a default case for codes added later. An unknown failure must remain a failure. Message and attachment actions fail with ClientClosed after the owning client ends.