Skip to content

React Native SDK Upgrade Guide

Every pre-8.0.0 React Native app must migrate. The npm package name remains @xmtp/react-native-sdk; version 8.0.0-rc4 uses the generated native v8 API. Existing method names, option shapes, generic types, hooks, and database files are not a compatibility contract.

Start with Migrate to a self-hosted backend. All participants must use the same backend. Keep the other SDK and backend version pins from that guide. Follow React Native and Expo to configure Hermes, New Architecture, native builds, and deployment floors.

This guide lists changes from xmtp-react-native v5.7.0 to @xmtp/react-native-sdk 8.0.0-rc4, built from sdks/react-native in xmtp/libxmtp. A call can keep its name and still have different parameters, results, or behavior. The Before examples use v5.7.0. The After examples use the v8 declarations and are checked as complete TypeScript programs.

Use SDK 8.0.0-rc4 with backend 8.0.0-rc4:

Terminal window
npm install --save-exact @xmtp/[email protected]

Install iOS pods and rebuild the native app. Expo apps need a rebuilt development client or prebuilt native project. Expo Go and JavaScript-only updates cannot install the module. Import the public API from the package root and optional React helpers from /react. Do not import generated binding paths.

Version 5.7.0 was an Expo module. Version 8 is a Hermes JSI TurboModule with prebuilt Rust libraries. It requires Hermes and New Architecture. It no longer needs expo as a peer dependency, and it does not accept a viem WalletClient as a signer. Remove app imports from @xmtp/proto; v8 has its own EncodedContent and ContentTypeId records.

Areav8 integration
BackendPass backend: { url } or a Backend. Use the chosen deployment; there is no implicit public network.
SignerImplement async identity(), kind(), and sign(request), or restore a saved LocalSigningKey.
StoragePass explicit storage options. Keep the optional 32-byte encryption key, location, and label stable. Default storage uses the native app directory.
Client creationUse Client.create(signer, options) or reopen a current database with Client.build(identity, options).
ShutdownAwait client.end() before external file operations.
TypesUse Client, Group, Dm, Message, and Conversation without legacy generic parameters.
ValuesIDs are strings; bytes are Uint8Array; maps are Map; full-width integers use bigint; times use Timestamp.
ContentNarrow message.content.kind. Standard codecs are built in; register only app custom codecs in ClientOptions.codecs.
ReadersUse SDK message/conversation readers. Await app processing before the next value; close readers when their owner changes.
ReactUse XmtpProvider, useXmtp, and useClient from /react. No setClient, message hooks, or conversation hooks are exported.
AttachmentsUse client.attachments to create, upload, download, and manage native files. The app owns picking media and OS permissions.
PushObtain tokens and OS permissions in the app, then call client.enableNotifications. Rewrite native handlers for the current push payload.

The sections below give the call-by-call changes. The TypeScript shapes match the Node SDK Upgrade Guide, with mobile storage and lifecycle integration. Use the React Native API reference for the exact exported declarations.

v5.7.0 valuev8 value
new PublicIdentity(address, "ETHEREUM") class that lowercases the identifierPlain PublicIdentity record { identifier, kind: "ethereum" }. Use "passkey" for a passkey and "handle" for a handle.
Upper-case string values such as "ETHEREUM", "PUBLISHED", and "DESCENDING"Lower camel case strings such as "ethereum", "published", and "descending".
XMTPEnvironment ("local", "dev", "production")Removed. Pass the deployment URL in backend. Use storage.label if the app keeps separate databases per deployment.
Branded InboxId, InstallationId, ConversationId, MessageId, and ConversationTopicIDs remain strings. InstallationId values are hex strings.
Nanosecond number fields such as sentNs, insertedAtNs, and exportedAtNsTimestamp. Use .ns for a bigint, .date for a Date, and new Timestamp(ns) for an input. Old number nanosecond values lose precision; v8 uses bigint.
Plain objects keyed by string, such as canMessage and getHmacKeys resultsMap. Read entries with .get(key).
Generic Client<ContentTypes>, Group<ContentTypes>, Dm<ContentTypes>, Conversation<ContentTypes>, DecodedMessage<T>, and DecodedMessageV2<T>Classes Client, Group, Dm, and Message. Conversation is the type Group | Dm. Narrow a conversation with instanceof Group or conversation.kind. Narrow content with content.kind.
ConversationVersion.GROUP / .DM and conversation.versionconversation.kind is "group" or "dm".
Public constructors such as new Client(...), new Group(...), and new DecodedMessage(...)Use Client.create, Client.build, and SDK lookup or creation methods. Public SDK objects have private constructors.
Client.codecRegistry and Client.register(codec)Removed. Pass custom codecs in ClientOptions.codecs. Standard content types are built in.
emitter, EventTypes, and expo-modules-core subscriptionsRemoved. Use stream objects and client.events(...).

A signer has three asynchronous methods: identity(), kind(), and sign(request). Replace getIdentifier, getChainId, getBlockNumber, signerType, and signMessage. sign returns signature bytes, not a hex string, and v8 does not split an ECDSA signature for you.

Before:

Before (xmtp-react-native v5.7.0)
import { PublicIdentity, type Signer } from "@xmtp/react-native-sdk";
export function createSigner(
address: string,
signText: (text: string) => Promise<string>,
): Signer {
return {
getIdentifier: async () => new PublicIdentity(address, "ETHEREUM"),
getChainId: () => undefined,
getBlockNumber: () => undefined,
signerType: () => "EOA",
signMessage: async (message) => ({ signature: await signText(message) }),
};
}

After:

const
const signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
= {
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 ({
text: string
text
}) => ({
kind: "ecdsa"
kind
: "ecdsa",
value: Uint8Array<ArrayBufferLike>
value
: await
signText: (text: string) => Promise<Uint8Array>
signText
(
text: string
text
) }),
};

For a smart contract wallet, replace signerType: () => "SCW", getChainId, and getBlockNumber with fields in the result of kind(). Return the signature bytes, address, chain ID, and optional block number from sign(). Chain IDs and block numbers are unsigned 64-bit bigint values.

const
const signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
= {
Signer.identity(): Promise<PublicIdentity>
identity
: async () => ({
identifier: string
identifier
:
address: string
address
,
kind: PublicIdentityKind
kind
: "ethereum" }),
Signer.kind(): Promise<SignerKind>
kind
: async () => ({
kind: "scw"
kind
: "scw",
chainId: bigint
chainId
,
blockNumber?: bigint | undefined
blockNumber
}),
Signer.sign(request: SigningRequest): Promise<Signature>
sign
: async ({
text: string
text
}) => ({
kind: "scw"
kind
: "scw",
bytes: Uint8Array<ArrayBufferLike>
bytes
: await
signText: (text: string) => Promise<Uint8Array>
signText
(
text: string
text
),
address: string
address
,
chainId: bigint
chainId
,
blockNumber?: bigint | undefined
blockNumber
,
}),
};

Version 5.7.0 accepted a viem WalletClient and converted it. In v8, write a Signer that calls the wallet and converts its hex signature to bytes. Passkey signers return a "passkey" signature with signature, publicKey, authenticatorData, and clientDataJson byte buffers, rather than the old SignedData strings.

Client.createRandom(options) is removed. Generate a local key, save its bytes in secure storage, and pass it to Client.create. Restore it with LocalSigningKey.fromBytes on the next launch:

const
const signer: LocalSigningKey
signer
=
savedKey: Uint8Array<ArrayBufferLike> | undefined
savedKey
? await
class LocalSigningKey
LocalSigningKey
.
LocalSigningKey.fromBytes(key: Uint8Array): Promise<LocalSigningKey>
fromBytes
(
savedKey: Uint8Array<ArrayBufferLike>
savedKey
)
: 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.generateKeys(): Promise<LocalSigningKey>
generateKeys
();
const
const keyToSave: Uint8Array<ArrayBufferLike>
keyToSave
= await
const signer: LocalSigningKey
signer
.
LocalSigningKey.toBytes(): Promise<Uint8Array>
toBytes
();

Before:

Before (xmtp-react-native v5.7.0)
import {
Client,
ReactionCodec,
RemoteAttachmentCodec,
ReplyCodec,
type Signer,
} from "@xmtp/react-native-sdk";
export async function createClient(signer: Signer, key: Uint8Array) {
return Client.create(signer, {
env: "production",
dbEncryptionKey: key,
appVersion: "chat/5.7.0",
deviceSyncEnabled: true,
codecs: [new ReactionCodec(), new ReplyCodec(), new RemoteAttachmentCodec()],
});
}

After:

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.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/8.0.0" },
storage: StorageOptions
storage
: {
location: StorageLocation
location
: "default",
label?: string | undefined
label
: "chat",
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
key: Uint8Array<ArrayBufferLike>
key
},
deviceSync?: boolean | undefined
deviceSync
: true,
registration?: RegistrationOptions | undefined
registration
: {
auto?: boolean | undefined
auto
: true },
});
v5.7.0 option or helperv8 option or helper
env, customLocalHost, gatewayHostbackend: { url } or a Backend from await Backend.connect({ url, appVersion?, credentials? }). Include the scheme. A device needs a URL it can reach; localhost on a phone is the phone.
Top-level appVersionbackend.appVersion.
Required dbEncryptionKey (32 bytes)Optional storage.encryptionKey: Uint8Array (32 bytes). Keep the same key when you reopen a database.
dbDirectory omittedstorage: { location: "default" }. The SDK uses the native app directory.
dbDirectory: pathstorage.location: { directory }, or { dbPath, attachmentsDir } for explicit files. Both explicit paths are required.
Environment in the database file namestorage.label. Keep the label and storage location stable. A label names a directory; it does not select a network.
deviceSyncEnableddeviceSync. It defaults to true.
debugEventsEnabledRemoved. Use client.diagnostics and logging.
preAuthenticateToInboxCallbackhandlers.preAuthenticate: an object with an asynchronous run() method.
forkRecoveryOptions: { enableRecoveryRequests, groupsToRequestRecovery, disableRecoveryResponses, workerIntervalNs }forkRecovery: { policy, groups, disableResponses, workerIntervalNs }. The policy is "none", "allowlistedGroups", or "all", rather than "groups". The interval is a bigint.
codecs: [new ReactionCodec(), new ReplyCodec(), ...]Omit standard codecs. Pass only custom codecs in codecs; see codecs.
No equivalentregistration: { auto, nonce }, allowOffline, workers, and attachments. Automatic registration defaults to true. New identity generation uses nonce 0.
Client.ffiCreateClient(identity, options) then ffiRegisterIdentity()Client.create with registration: { auto: false }, then the signature request flow, or client.register().

Client.build(identity, options, inboxId?) keeps its form and reopens a client without a signer. It now requires the v8 options object.

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
, {
backend?: BackendSource | undefined
backend
: {
url: string
url
: "https://xmtp.example.com" },
storage: StorageOptions
storage
: {
location: StorageLocation
location
: {
directory: string
directory
},
label?: string | undefined
label
: "chat",
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
key: Uint8Array<ArrayBufferLike>
key
},
});

The v5.7.0 database is not an input to this migration. Create a new database for the deployment. See the database migration rules before choosing paths.

Configure backend credentials separately from the identity signer:

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.create(signer: Signer, options: ClientOptions): Promise<Client>
create
(
signer: Signer
signer
, {
backend?: BackendSource | undefined
backend
: {
url: string
url
: "https://xmtp.example.com",
credentials?: CredentialSource | Credential | undefined
credentials
: {
async
CredentialSource.credential(): Promise<Credential>
credential
() {
const
const credential: {
token: string;
expiresAtSeconds: bigint;
}
credential
= await
fetchToken: () => Promise<{
token: string;
expiresAtSeconds: bigint;
}>
fetchToken
();
return {
value: string
value
: `Bearer ${
const credential: {
token: string;
expiresAtSeconds: bigint;
}
credential
.
token: string
token
}`,
expiresAtSeconds: bigint
expiresAtSeconds
:
const credential: {
token: string;
expiresAtSeconds: bigint;
}
credential
.
expiresAtSeconds: bigint
expiresAtSeconds
,
};
},
},
},
storage: StorageOptions
storage
: {
location: StorageLocation
location
: "default",
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
key: Uint8Array<ArrayBufferLike>
key
},
});

backend.credentials accepts a Credential or a source with asynchronous credential(). A credential has value, expiresAtSeconds as a bigint in Unix seconds, and an optional header name. Use client.setCredential(credential) to replace a supplied credential. Client.fetchServerConfiguration(backend) reads deployment limits without a client. client.serverConfiguration is a property; await client.refreshServerConfiguration() stores configuration for the next build.

Static calls take a BackendSource as the last argument: { url, appVersion?, credentials? } or a connected Backend. In v5.7.0 the environment was the first argument.

const
const backend: Backend
backend
= await
class Backend
Backend
.
Backend.connect(options: BackendOptions): Promise<Backend>
connect
({
url: string
url
: "https://xmtp.example.com" });
const
const availability: Map<string, boolean>
availability
= 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
],
const backend: Backend
backend
);
const
const available: boolean | undefined
available
=
const availability: Map<string, boolean>
availability
.
Map<string, boolean>.get(key: string): boolean | undefined

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.

get
(
`${
identity: PublicIdentity
identity
.
kind: PublicIdentityKind
kind
}:${
identity: PublicIdentity
identity
.
identifier: string
identifier
.
String.toLowerCase(): string

Converts all the alphabetic characters in a string to lowercase.

toLowerCase
()}`,
);
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
([
inboxId: string
inboxId
],
const backend: Backend
backend
);
const
const counts: Map<string, bigint>
counts
= await
function latestInboxUpdatesCount(inboxIds: Array<InboxId>, backend: BackendSource): Promise<Map<string, bigint>>
latestInboxUpdatesCount
([
inboxId: string
inboxId
],
const backend: Backend
backend
);
v5.7.0 callv8 call and result
client.publicIdentityclient.identity: PublicIdentity.
client.dbPath, client.environmentclient.storagePath (can be absent for in-memory storage). environment is removed.
client.inboxId, client.installationIdSame properties. client.installationIdBytes, isInMemory, libxmtpVersion, appVersion, and options are new.
Client.canMessage(env, identities)Client.canMessage(identities, backend): Promise<Map<string, boolean>>. Keys are ethereum:<normalized address> or passkey:<lowercase key hex>.
client.canMessage(identities) plain objectSame name. Returns a Map with the keys above.
Client.getOrCreateInboxId(identity, env)Client.inboxIdFor(identity, backend): Promise<string>. When no inbox holds the identity, it returns the nonce-0 inbox ID only if that inbox has no identity updates or still holds the identity, and fails with IdentityNotFound otherwise. Use canMessage to test registration.
client.findInboxIdFromIdentity(identity)client.inboxIdFor(identity): Promise<string | undefined>.
Client.inboxStatesForInboxIds(env, ids)Client.inboxStates(ids, backend): Promise<InboxState[]>.
client.inboxState(refresh)Same.
client.inboxStates(refresh, ids)client.inboxStates(ids, refresh). The argument order is reversed.
client.debugInformation.getKeyPackageStatuses(ids)client.keyPackageStatuses(ids): Promise<Map<string, KeyPackageStatus>>. Client.keyPackageStatuses(ids, backend) is new.
client.addAccount(signer, allowReassignInboxId = false)client.unsafeAddAccount(signer, allowInboxReassign). Supply the boolean explicitly.
client.removeAccount(signer, identity)Same form: removeAccount(recoverySigner, identity).
client.revokeInstallations(signer, ids), revokeAllOtherInstallations(signer)Same forms. Installation IDs are hex strings. The signer is no longer nullable.
Client.revokeInstallations(env, signer, inboxId, ids)Client.revokeInstallations(signer, inboxId, ids, backend).
No equivalentclient.changeRecoveryIdentifier(signer, identity), Client.isAddressAuthorized, Client.isInstallationAuthorized, and Client.newestMessageMetadata. The new recovery identity must already be an identity of the inbox; see Remove an identity.
client.signWithInstallationKey(message)Same.
client.verifySignature(message, signature)client.verifySignedWithInstallationKey(text, signature). Client.verifySignedWithPublicKey(text, signature, publicKey) is new.
Client.dropClient(installationId)await client.end().
client.deleteLocalDatabase()await client.storage.delete_(). It ends the client, then removes its database.
client.dropLocalDatabaseConnection() / reconnectLocalDatabase()No separate disconnect; await client.end() releases the database. reconnectLocalDatabase() becomes await client.storage.reconnect(). await client.storage.path() returns the database path.
const
const registered: boolean
registered
= await
client: Client
client
.
ClientMembers.isRegistered(): Promise<boolean>
isRegistered
();
const
const state: InboxState
state
= await
client: Client
client
.
ClientMembers.inboxState(refreshFromNetwork: boolean): Promise<InboxState>
inboxState
(true);
const
const states: InboxState[]
states
= await
client: Client
client
.
ClientMembers.inboxStates(ids: Array<InboxId>, refreshFromNetwork: boolean): Promise<Array<InboxState>>
inboxStates
([
client: Client
client
.
ClientMembers.inboxId: string
inboxId
], false);
const
const inboxId: string | undefined
inboxId
= await
client: Client
client
.
ClientMembers.inboxIdFor(identity: PublicIdentity): Promise<InboxId | undefined>
inboxIdFor
(
identity: PublicIdentity
identity
);
await
client: Client
client
.
ClientMembers.removeAccount(recoverySigner: Signer, identity: PublicIdentity): Promise<void>
removeAccount
(
recoverySigner: Signer
recoverySigner
,
identity: PublicIdentity
identity
);
await
client: Client
client
.
ClientMembers.revokeInstallations(signer: Signer, ids: Array<InstallationId>): Promise<void>
revokeInstallations
(
recoverySigner: Signer
recoverySigner
,
installationIds: string[]
installationIds
);
await
client: Client
client
.
ClientMembers.revokeAllOtherInstallations(signer: Signer): Promise<void>
revokeAllOtherInstallations
(
recoverySigner: Signer
recoverySigner
);
v5.7.0 resultv8 result
InboxState class with PublicIdentity instancesRead-only InboxState record. creationSignatureKind uses "erc191", "erc1271", "installationKey", or "p256". handle and otherHandles are new.
Installation.createdAt in millisecondscreatedAt?: Timestamp.
KeyPackageStatuses.statuses mapMap<string, KeyPackageStatus>. lifetime.notBefore and notAfter are Unix-second bigint values and can be absent.
GroupSyncSummary counts as numberseligible and synced as bigint.

The ffi* signature methods become a SignatureRequest:

v5.7.0 callv8 call
ffiCreateSignatureText()unsafeCreateInboxSignatureRequest(), then request.signatureText(). The request can be absent.
ffiAddIdentitySignatureText(identity, allowReassign)unsafeAddAccountSignatureRequest(identity, allowInboxReassign).
ffiRemoveIdentitySignatureText(identity)unsafeRemoveAccountSignatureRequest(identity).
ffiRevokeInstallationsSignatureText(ids)unsafeRevokeInstallationsSignatureRequest(ids).
ffiRevokeAllOtherInstallationsSignatureText()unsafeRevokeAllOtherInstallationsSignatureRequest(). The request can be absent.
ffiAddEcdsaSignature(bytes) / ffiAddScwSignature(bytes, address, chainId, block?)request.addSignature({ kind: "ecdsa", value }) or a "scw" signature, or await request.sign(signer).
ffiApplySignature() / Client.ffiApplySignatureRequest(env, type)client.unsafeApplySignatureRequest(request).
ffiRegisterIdentity()client.unsafeApplySignatureRequest(request) for a created request, or client.register().
Client.ffiRevokeInstallationsSignatureText(env, identity, inboxId, ids)Use Client.revokeInstallations(signer, inboxId, ids, backend).

For manual registration, create with registration: { auto: false }, sign the returned request, then apply it:

const
const request: SignatureRequest | undefined
request
= await
client: Client
client
.
ClientMembers.unsafeCreateInboxSignatureRequest(): Promise<SignatureRequest | undefined>
unsafeCreateInboxSignatureRequest
();
if (
const request: SignatureRequest | undefined
request
) {
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
);
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const text: string
text
);
}

Creation methods accept inbox IDs or identities through one method. Do not mix strings and identities in one member list.

Before:

Before (xmtp-react-native v5.7.0)
import { PublicIdentity, type Client } from "@xmtp/react-native-sdk";
export async function createConversations(client: Client, address: string) {
const peer = new PublicIdentity(address, "ETHEREUM");
const dm = await client.conversations.findOrCreateDmWithIdentity(peer);
const group = await client.conversations.newGroupWithIdentities([peer], {
name: "Project",
description: "Project chat",
permissionLevel: "admin_only",
});
const draft = await client.conversations.newGroupOptimistic({ name: "Draft" });
await draft.addMembersByIdentity([peer]);
return { dm, group, draft };
}

After:

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
(
inboxIds: string[]
inboxIds
, {
name?: string | undefined
name
: "Project",
imageUrl?: string | undefined
imageUrl
: "https://example.com/project.png",
description?: string | undefined
description
: "Project chat",
permissions?: GroupPermissionMode | undefined
permissions
: {
kind: "adminOnly"
kind
: "adminOnly" },
appData?: string | undefined
appData
: "{}",
});
const
const draft: Group
draft
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.createGroupOptimistic(options?: CreateGroupOptions): Promise<Group>
createGroupOptimistic
({
name?: string | undefined
name
: "Draft",
});
const
const result: MembershipResult
result
= await
const draft: Group
draft
.
Group.addMembers(members: Array<InboxId> | Array<PublicIdentity>): Promise<MembershipResult>
addMembers
([
peer: PublicIdentity
peer
]);
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const result: MembershipResult
result
.
added: string[]
added
,
const result: MembershipResult
result
.
failedInstallationIds: string[]
failedInstallationIds
);
v5.7.0 methodv8 method
newGroup(ids, opts), newGroupWithIdentities(identities, opts)createGroup(ids | identities, options?): Promise<Group>.
newGroupCustomPermissions(ids, policySet, opts) and the WithIdentities formcreateGroup(ids | identities, { permissions: { kind: "custom", policySet } }).
newGroupOptimistic(opts)createGroupOptimistic(options?).
findOrCreateDm(inboxId, settings?), newConversation(inboxId, settings?), findOrCreateDmWithIdentity(identity, settings?)createDm(inboxId | identity, { disappearing }?): Promise<Dm>.
findGroup(id), findConversation(id)getById(id): Promise<Conversation | undefined>. Check instanceof Group for a group.
findConversationByTopic(topic)Removed. Topics have a new format; use the conversation ID with getById.
findDmByInboxId(id), findDmByIdentity(identity)getDmByInboxId(id), getDmByIdentity(identity).
findMessage(id)getMessageById(id): Promise<Message | undefined>.
list(opts, limit, consentStates, createdAfterNs, createdBeforeNs, lastActivityAfterNs, lastActivityBeforeNs, orderBy)list(options?) with one ListConversationsOptions object. listGroups(options | undefined) and listDms(options | undefined) require the argument; pass undefined for defaults.
ConversationOptions field selection, such as { lastMessage: true }Removed. Read await conversation.lastMessage() or await conversation.state().
syncAllConversations(consentStates?)syncAll(consentStates | undefined): Promise<GroupSyncSummary>.
sync()Same.
fromWelcome(encrypted), group.processMessage(encrypted)Removed. A push contains a hint; use conversations.sync(), conversation.sync(), then local message queries.
getHmacKeys() / getAllPushTopics()conversations.hmacKeys(): Promise<Map<string, HmacKey[]>>. Push topics are removed; see notifications.

Conversation lists return at most 100 conversations when you pass no limit. A limit below 1 or above 1000 fails with InvalidArgument. To read every conversation, order by creation time and pass the last conversation’s createdAt as createdBefore and its ID as beforeId. See Paginate conversations.

Message lists return the 100 newest matching messages, in the requested order, when you pass no limit. A limit below 1 or above 1000 fails with InvalidArgument. To read more, page with messageHistoryPage. See Paginate messages.

Lookups have the same local-only behavior. Sync first when the app needs the latest network state:

const
const conversation: Conversation | undefined
conversation
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.getById(id: ConversationId): Promise<Conversation | undefined>
getById
(
id: string
id
);
const
const dm: Dm | undefined
dm
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.getDmByInboxId(peer: InboxId): Promise<Dm | undefined>
getDmByInboxId
(
peerInboxId: string
peerInboxId
);
const
const message: Message | undefined
message
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.getMessageById(id: MessageId): Promise<Message | undefined>
getMessageById
(
id: string
id
);

Group and DM fields moved into a state snapshot. Read it with await group.state() or await dm.state():

v5.7.0 field or callv8 field or call
group.groupName, groupImageUrl, groupDescription, name(), imageUrl(), description(), appData()(await group.state()) fields name, imageUrl, description, and appData.
isGroupActive, isActive(), pausedForVersion()(await group.state()).common.isActive and .pausedForVersion. For a DM, read (await dm.state()).
conversation.state and consentState().common.consentState for a group, or consentState on DM state.
updateConsent(state)updateConsentState(state).
permissionPolicySet()(await group.state()).permissions. policyType is "allMembers", "adminOnly", or "custom".
listAdmins(), listSuperAdmins(), isAdmin(id), isSuperAdmin(id)Same methods. The state snapshot also has admins and superAdmins arrays.
memberInboxIds()(await group.members()).map((member) => member.inboxId).
members()Same name. permissionLevel uses "superAdmin", rather than "super_admin". handle and otherHandles are new.
addMembers(ids), addMembersByIdentity(identities)addMembers(ids | identities): Promise<MembershipResult>. addedMembers becomes added; removedMembers becomes removed.
removeMembers(ids), removeMembersByIdentity(identities)removeMembers(ids | identities).
creatorInboxId()group.creatorInboxId property; null when unknown. addedByInboxId and isCreator are also properties.
peerInboxId()await dm.peerInboxId(): string | null. dm.duplicateDms() and group.peerInboxIds() are new.
leaveGroup()requestRemoval(). (await group.state()).membershipState reports "pendingRemove".
createdAt numbercreatedAt: Timestamp.
topictopic is [group_message_v1/<hex-id>]. See Topics.
getConversationHmacKeys()hmacKeys(): Promise<HmacKey[]>.
getConversationPushTopics()Removed. Use setNotifications for per-conversation overrides.
getDebugInformation(), commitLogForkStatusdebugInfo(), and .common.commitLogForkStatus in state.
updateAppData(value)updateAppData(value, expected | undefined). Pass undefined for an unguarded write.

updateName, updateImageUrl, updateDescription, addAdmin, removeAdmin, addSuperAdmin, removeSuperAdmin, sync, and deleteMessage keep their call forms. Group methods membershipCapabilities() and lastActivityAt(contentTypes) are new.

const
const snapshot: GroupState
snapshot
= await
group: Group
group
.
Group.state(): Promise<GroupState>
state
();
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const snapshot: GroupState
snapshot
.
name: string
name
,
const snapshot: GroupState
snapshot
.
common: ConversationState
common
.
isActive: boolean
isActive
,
const snapshot: GroupState
snapshot
.
permissions: GroupPermissions
permissions
);
const
const members: Member[]
members
= await
group: Group
group
.
Group.members(): Promise<Array<Member>>
members
();
for (const
const member: Member
member
of
const members: Member[]
members
)
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const member: Member
member
.
inboxId: string
inboxId
,
const member: Member
member
.
identities: PublicIdentity[]
identities
);
const
const peer: string | null
peer
= await
dm: Dm
dm
.
Dm.peerInboxId(): Promise<InboxId | null>
peerInboxId
();
const
const admins: string[]
admins
= await
group: Group
group
.
Group.listAdmins(): Promise<Array<InboxId>>
listAdmins
();
const
const creator: string | null
creator
=
group: Group
group
.
Group.creatorInboxId: string | null
creatorInboxId
;
const
const isAdmin: boolean
isAdmin
=
const creator: string | null
creator
=== null ? false : await
group: Group
group
.
Group.isAdmin(inboxId: InboxId): Promise<boolean>
isAdmin
(
const creator: string
creator
);
await
group: Group
group
.
Group.updateName(value: string): Promise<void>
updateName
("Project team");
await
group: Group
group
.
Group.updatePermission(kind: PermissionUpdateKind, policy: PermissionPolicy, metadataField: MetadataFieldKind | undefined): Promise<void>
updatePermission
("updateMetadata", "admin", "name");

Permission methods become one call:

v5.7.0 callv8 call
updateAddMemberPermission, updateRemoveMemberPermission, updateAddAdminPermission, updateRemoveAdminPermissionupdatePermission("addMember" | "removeMember" | "addAdmin" | "removeAdmin", policy, undefined).
updateNamePermission, updateDescriptionPermission, updateImageUrlPermissionupdatePermission("updateMetadata", policy, "name" | "description" | "imageUrl").
No equivalentupdatePermission("updateMetadata", policy, "disappearing" | "appData").
Option values "allow", "deny", "admin", "super_admin"Policy values "allow", "deny", "admin", "superAdmin".

Creation and list options also changed:

v5.7.0 fieldv8 field
permissionLevel: "all_members" / "admin_only"permissions: { kind: "allMembers" } / { kind: "adminOnly" }.
name, imageUrl, description, appDataSame names.
disappearingMessageSettings: new DisappearingMessageSettings(startNs, retentionNs)disappearing: { from: Timestamp, retentionNs: bigint }.
PermissionPolicySet.addMemberPolicy, removeMemberPolicy, addAdminPolicy, removeAdminPolicyaddMember, removeMember, addAdmin, removeAdmin.
updateGroupNamePolicy, updateGroupDescriptionPolicy, updateGroupImagePolicy, updateMessageDisappearingPolicy, updateAppDataPolicyupdateName, updateDescription, updateImage, updateDisappearing, updateAppData.
Positional limit, consentStateslimit, consentStates fields.
Positional createdAfterNs, createdBeforeNs, lastActivityAfterNs, lastActivityBeforeNscreatedAfter, createdBefore, lastActivityAfter, lastActivityBefore, each a Timestamp.
Positional orderBy: "created_at" / "last_activity"orderBy: "createdAt" / "lastActivity".
No equivalentkind: "group" | "dm" and includeDuplicateDms.
const
const groups: Group[]
groups
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.listGroups(options: ListConversationsOptions | undefined): Promise<Array<Group>>
listGroups
({
consentStates?: ConsentState[] | undefined
consentStates
: ["allowed"],
createdAfter?: Timestamp | undefined
createdAfter
: new
new Timestamp(ns: bigint): Timestamp
Timestamp
(0n),
orderBy?: ConversationOrder | undefined
orderBy
: "lastActivity",
limit?: number | undefined
limit
: 50,
});
const
const summary: GroupSyncSummary
summary
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.syncAll(consentStates: Array<ConsentState> | undefined): Promise<GroupSyncSummary>
syncAll
(["allowed"]);
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const summary: GroupSyncSummary
summary
.
eligible: bigint
eligible
,
const summary: GroupSyncSummary
summary
.
synced: bigint
synced
);

Replace the disappearing-message methods:

v5.7.0 callv8 call
disappearingMessageSettings() / isDisappearingMessagesEnabled()State fields disappearingSettings / isDisappearingEnabled, in group .common or DM state.
updateDisappearingMessageSettings(settings)updateDisappearingSettings({ from: new Timestamp(fromNs), retentionNs }).
clearDisappearingMessageSettings()updateDisappearingSettings(undefined). The argument is required.
await
group: Group
group
.
Group.updateDisappearingSettings(settings: DisappearingSettings | undefined): Promise<void>
updateDisappearingSettings
({
from: Timestamp
from
: new
new Timestamp(ns: bigint): Timestamp
Timestamp
(
fromNs: bigint
fromNs
),
retentionNs: bigint
retentionNs
: 86_400_000_000_000n,
});
const
const settings: DisappearingSettings | undefined
settings
= (await
group: Group
group
.
Group.state(): Promise<GroupState>
state
()).
common: ConversationState
common
.
disappearingSettings?: DisappearingSettings | undefined
disappearingSettings
;
await
group: Group
group
.
Group.updateDisappearingSettings(settings: DisappearingSettings | undefined): Promise<void>
updateDisappearingSettings
(
var undefined
undefined
);

v8 also exposes typed metadata fields for app data and per-inbox values. On a group or DM, use metadataFields(), metadataField(name), metadataValue(field), metadataValues(fields), mapValue(field, key), userData(fields, inboxIds), updateUserData(values), and updateMetadataField(field, operation). Both userData arguments are required; pass undefined for all fields or inboxes.

const
const field: MetadataFieldRef
field
=
function metadataFieldRef(field: WellKnownMetadataField): MetadataFieldRef
metadataFieldRef
("userDisplayName");
await
group: Group
group
.
Group.updateUserData(values: Array<UserFieldUpdate>): Promise<void>
updateUserData
([
{
field: MetadataFieldRef
field
,
value?: FieldValue | undefined
value
: {
kind: "string"
kind
: "string",
value: string
value
: "Sam" } },
]);
const
const profiles: Map<string, UserFieldValue[]>
profiles
= await
group: Group
group
.
Group.userData(fields: Array<MetadataFieldRef> | undefined, inboxIds: Array<InboxId> | undefined): Promise<Map<InboxId, Array<UserFieldValue>>>
userData
([
const field: MetadataFieldRef
field
],
var undefined
undefined
);
const
const fields: MetadataFieldDescriptor[]
fields
= await
group: Group
group
.
Group.metadataFields(): Promise<Array<MetadataFieldDescriptor>>
metadataFields
();

Version 5.7.0 sent standard content through send with a native content key, such as { reaction: ... } or { reply: ... }. Version 8 has a typed helper for each standard content type: sendText, sendMarkdown, sendReaction, sendReply, sendReadReceipt, sendAttachment, sendRemoteAttachment, sendMultiRemoteAttachment, sendTransactionReference, sendWalletSendCalls, sendActions, and sendIntent. Each returns a message ID.

v5.7.0 call or valuev8 call or value
send("Hello") or send({ text: "Hello" })sendText("Hello", options?).
send({ reaction: { reference, referenceInboxId, action, schema, content } })sendReaction(reference, referenceInboxId | undefined, { action, content, schema }, options?), or message.react(...).
send({ reply: { reference, content: { text } } })sendReply(reference, referenceInboxId | undefined, encoded, options?), or message.reply("text").
send({ readReceipt: {} })sendReadReceipt(options?).
send({ attachment: { filename, mimeType, data } }) with base64 datasendAttachment({ filename, mimeType, content }) with content: Uint8Array.
send({ remoteAttachment }), send({ multiRemoteAttachment })sendRemoteAttachment(remote), sendMultiRemoteAttachment(value). See attachments.
send(value, { contentType }) with a registered JS codecsend(codec, value, options?). The codec does not need to be registered to send.
SendOptions.contentType, shouldPushSendOptions has shouldPush?, optimistic?, idempotencyKey?, and compression?: "deflate" | "gzip". Without shouldPush, the content type’s default applies.
prepareMessage(content, opts?, noSend?)prepareMessage(encoded | codec, ...). There is no noSend flag; see the note below.
publishPreparedMessages()publishMessages(). publishMessage(id) remains.
deleteMessage(id)Same, on a conversation or client.conversations. message.delete() and deleteLocally() are new.

Before:

Before (xmtp-react-native v5.7.0)
import type { Group, InboxId, MessageId } from "@xmtp/react-native-sdk";
export async function reply(
group: Group,
messageId: MessageId,
senderInboxId: InboxId,
) {
await group.send("Hello");
await group.send({
reaction: {
reference: messageId,
referenceInboxId: senderInboxId,
action: "added",
schema: "unicode",
content: "👍",
},
});
await group.send({
reply: { reference: messageId, content: { text: "Reply" } },
});
}

After:

const
const id: string
id
= await
group: Group
group
.
Group.sendText(text: string, options?: SendOptions): Promise<MessageId>
sendText
("Hello", {
shouldPush?: boolean | undefined
shouldPush
: false });
await
group: Group
group
.
Group.sendReaction(reference: MessageId, referenceInboxId: InboxId | undefined, reaction: Reaction, options?: SendOptions): Promise<MessageId>
sendReaction
(
parent: Message
parent
.
MessageFields.id: string
id
,
parent: Message
parent
.
MessageFields.senderInboxId: string
senderInboxId
, {
content: string
content
: "👍",
action: ReactionAction
action
: "added",
schema: ReactionSchema
schema
: "unicode",
});
await
group: Group
group
.
Group.sendReply(reference: MessageId, referenceInboxId: InboxId | undefined, content: EncodedContent, options?: SendOptions): Promise<MessageId>
sendReply
(
parent: Message
parent
.
MessageFields.id: string
id
,
parent: Message
parent
.
MessageFields.senderInboxId: string
senderInboxId
,
new
new TextCodec(): TextCodec
TextCodec
().
StandardCodec<string, string>.encode(value: string): EncodedContent
encode
("Reply"),
);
await
parent: Message
parent
.
Message.reply(content: string | EncodedContent, options?: SendOptions): Promise<MessageId> (+1 overload)
reply
("Another reply");
await
parent: Message
parent
.
Message.react(reaction: Reaction, options?: SendOptions): Promise<MessageId>
react
({
content: string
content
: "👍",
action: ReactionAction
action
: "added",
schema: ReactionSchema
schema
: "unicode" });
const
const prepared: string
prepared
= await
group: Group
group
.
Group.prepareMessage<string>(codec: ContentCodec<string>, value: string, options?: SendOptions): Promise<MessageId> (+1 overload)
prepareMessage
(new
new TextCodec(): TextCodec
TextCodec
(), "Draft");
await
group: Group
group
.
Group.publishMessage(id: MessageId): Promise<void>
publishMessage
(
const prepared: string
prepared
);

prepareMessage stores an unpublished message, queues optimistic publication, and returns its ID. Background processing can publish it before an explicit publish call. Keep private drafts outside the SDK until the app is ready to send.

Standard codecs are built in. Do not pass ReactionCodec, ReplyCodec, RemoteAttachmentCodec, or the other v5.7.0 codecs in codecs. The native NativeContentCodec interface with contentKey is removed, and StaticAttachmentCodec becomes AttachmentCodec. The exported standard codecs are TextCodec, MarkdownCodec, ReadReceiptCodec, ReactionV2Codec, ReplyCodec, DeleteMessageCodec, AttachmentCodec, RemoteAttachmentCodec, MultiRemoteAttachmentCodec, TransactionReferenceCodec, WalletSendCallsCodec, ActionsCodec, IntentCodec, GroupUpdatedCodec, and LeaveRequestCodec.

A v5.7.0 JSContentCodec had contentType, encode, decode, fallback, and shouldPush. A v8 ContentCodec<T> has type, encode(value), and decode(encoded), with optional fallback(value) and shouldPush(value). EncodedContent.parameters is a Map<string, string>.

const
const codec: ContentCodec<Point>
codec
:
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 Point = {
readonly x: number;
readonly y: number;
}
Point
> = {
ContentCodec<Point>.type: ContentTypeId
type
: {
authorityId: string
authorityId
: "example.com",
typeId: string
typeId
: "point",
versionMajor: number
versionMajor
: 1,
versionMinor: number
versionMinor
: 0,
},
ContentCodec<Point>.encode: (value: Point) => EncodedContent
encode
: ({
x: number
x
,
y: number
y
}) => ({
type: ContentTypeId
type
:
const codec: ContentCodec<Point>
codec
.
ContentCodec<Point>.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
(`${
x: number
x
},${
y: number
y
}`),
}),
ContentCodec<Point>.decode: (encoded: EncodedContent) => Point
decode
: ({
content: Uint8Array<ArrayBufferLike>
content
}) => {
const
const parts: number[]
parts
= 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: Uint8Array<ArrayBufferLike>
content
).
String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)

Split a string into substrings using the specified separator and return them as an array.

@param ― separator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.

@param ― limit A value used to limit the number of elements returned in the array.

split
(",").
Array<string>.map<number>(callbackfn: (value: string, index: number, array: string[]) => number, thisArg?: any): number[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
(
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
);
if (
const parts: number[]
parts
.
Array<number>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
!== 2 ||
const parts: number[]
parts
.
Array<number>.some(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean

Determines whether the specified callback function returns true for any element of an array.

@param ― predicate A function that accepts up to three arguments. The some method calls the predicate function for each element in the array until the predicate returns a value which is coercible to the Boolean value true, or until the end of the array.

@param ― thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.

some
((
value: number
value
) => !
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.isFinite(number: unknown): boolean

Returns true if passed value is finite. Unlike the global isFinite, Number.isFinite doesn't forcibly convert the parameter to a number. Only finite values of the type number, result in true.

@param ― number A numeric value.

isFinite
(
value: number
value
))
) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Invalid point");
}
return {
x: number
x
:
const parts: number[]
parts
[0],
y: number
y
:
const parts: number[]
parts
[1] };
},
ContentCodec<Point>.fallback?: ((value: Point) => string | undefined) | undefined

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

fallback
: ({
x: number
x
,
y: number
y
}) => `Point ${
x: number
x
}, ${
y: number
y
}`,
ContentCodec<Point>.shouldPush?: ((value: Point) => boolean) | undefined

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

shouldPush
: () => true,
};

Register the codec on a client to decode received values:

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.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",
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
key: Uint8Array<ArrayBufferLike>
key
},
codecs?: readonly AnyContentCodec[] | undefined
codecs
: [
function pointCodec(): ContentCodec<Point>
pointCodec
()],
});

Send typed values, then check received custom content:

await
group: Group
group
.
Group.send<Point>(codec: ContentCodec<Point>, value: NoInfer<Point>, options?: SendOptions): Promise<MessageId> (+1 overload)
send
(
const codec: ContentCodec<Point>
codec
, {
x: number
x
: 1,
y: number
y
: 2 });
await
message: Message
message
.
Message.reply<Point>(codec: ContentCodec<Point>, value: NoInfer<Point>, options?: SendOptions): Promise<MessageId> (+1 overload)

Reply with a value of a typed codec. The codec's fallback applies to the nested envelope; the reply keeps the reply type's push default unless options.shouldPush is set. A failed codec step is CodecEncodeFailed, with no publish attempt.

reply
(
const codec: ContentCodec<Point>
codec
, {
x: number
x
: 3,
y: number
y
: 4 });
if (
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
=== "custom") {
const
const value: unknown
value
=
message: Message
message
.
Message.content: {
readonly kind: "custom";
readonly encoded: EncodedContent;
readonly rawBytes: Uint8Array;
readonly value?: unknown;
readonly error?: ErrorDetails;
}
content
.
value?: unknown
value
;
if (
typeof
const value: unknown
value
=== "object" &&
const value: object | null
value
!== null &&
"x" in
const value: object
value
&&
"y" in
const value: object & Record<"x", unknown>
value
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const value: object & Record<"x", unknown> & Record<"y", unknown>
value
.
x: unknown
x
,
const value: object & Record<"x", unknown> & Record<"y", unknown>
value
.
y: unknown
y
);
}
if (
message: Message
message
.
Message.content: {
readonly kind: "custom";
readonly encoded: EncodedContent;
readonly rawBytes: Uint8Array;
readonly value?: unknown;
readonly error?: ErrorDetails;
}
content
.
error?: ErrorDetails | undefined
error
)
var console: Console
console
.
Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

MDN Reference

error
(
message: Message
message
.
Message.content: {
readonly kind: "custom";
readonly encoded: EncodedContent;
readonly rawBytes: Uint8Array;
readonly value?: unknown;
readonly error?: ErrorDetails;
}
content
.
error?: ErrorDetails
error
);
}

messages, messagesWithReactions, and enrichedMessages become one messages(options?) call. Each Message carries its reactions, reply count, and reply parent.

v5.7.0 query or decoded fieldv8 query or decoded field
messages(opts), messagesWithReactions(opts), enrichedMessages(opts)messages(options?): Promise<Message[]>. lastMessage() and countMessages(options | undefined): Promise<bigint> are new.
beforeNs, afterNs, insertedBeforeNs, insertedAfterNssentBefore, sentAfter, insertedBefore, insertedAfter, each a Timestamp.
direction: "ASCENDING" / "DESCENDING"direction: "ascending" / "descending".
sortBy: "SENT_TIME" / "INSERTED_TIME"sortBy: "sentAt" / "insertedAt".
deliveryStatus: "ALL", "PUBLISHED", "UNPUBLISHED", "FAILED"Omit the field for all, or use "published", "unpublished", or "failed".
excludeContentTypes: string[]excludeContentTypes and contentTypes take ContentTypeId[], for example [standardContentType("reaction")]. excludeSenderInboxIds and limit remain. kind is new.
message.content() method and message.nativeContentmessage.content property: a MessageContent union. Narrow with content.kind. Text uses .value; replies use .referenceId and .body; reactions use .reaction.
message.contentTypeId string such as "xmtp.org/text:1.0"Optional contentType: ContentTypeId record. content.kind is usually enough.
sentNs, insertedAtNs; V2 sentAt: Date, sentAtNs, expiresAt, expiresAtNssentAt, insertedAt, optional expiresAt, each a Timestamp.
fallback; V2 fallbackTextOptional fallback.
childMessages; V2 reactions, hasReactions, reactionCountreactions: ReactionMessage[], with id, senderInboxId, sentAt, deliveryStatus, and reaction. reactions holds at most the 50 latest; reactionMessageCount counts every reaction message.
No equivalentreplyCount: bigint, inReplyTo, conversationId, kind, rawBytes, and deliveryCursor. await message.parent() reads the current parent.
Decode error thrown from content() for an unregistered type"custom" content keeps encoded, rawBytes, an optional decoded value, and an optional error. Malformed standard content is "unknown" with rawBytes and an error.

reactions holds at most the 50 latest reaction messages to a message. reactionMessageCount counts every unexpired reaction message to it, removals included, so it is not a per-emoji tally. Read older reactions with messageReactions(messageId, before, limit). See Reactions.

Before:

Before (xmtp-react-native v5.7.0)
import type { Group } from "@xmtp/react-native-sdk";
export async function read(group: Group) {
const messages = await group.enrichedMessages({
limit: 20,
afterNs: 0,
direction: "DESCENDING",
});
for (const message of messages) {
if (message.contentTypeId.typeId === "text") console.log(message.content());
console.log(message.sentAt, message.sentAtNs, message.reactionCount);
}
}

After:

const
const messages: Message[]
messages
= await
group: Group
group
.
Group.messages(options?: ListMessagesOptions): Promise<Array<Message>>
messages
({
limit?: number | undefined
limit
: 20,
sentAfter?: Timestamp | undefined
sentAfter
: new
new Timestamp(ns: bigint): Timestamp
Timestamp
(0n),
sortBy?: MessageSortBy | undefined
sortBy
: "sentAt",
direction?: MessageOrder | undefined
direction
: "descending",
contentTypes?: ContentTypeId[] | undefined
contentTypes
: [
function standardContentType(kind: StandardContentKind): ContentTypeId
standardContentType
("text")],
});
for (const
const message: Message
message
of
const messages: Message[]
messages
) {
if (
const message: Message
message
.
Message.content: MessageContent
content
.
kind: "text" | "markdown" | "readReceipt" | "reaction" | "attachment" | "remoteAttachment" | "multiRemoteAttachment" | "transactionReference" | "walletSendCalls" | "actions" | "intent" | "reply" | "groupUpdated" | "leaveRequest" | "deletedMessage" | "custom" | "unknown"
kind
=== "text")
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const message: Message
message
.
Message.content: {
readonly kind: "text";
readonly value: string;
}
content
.
value: string
value
);
if (
const message: Message
message
.
Message.content: MessageContent
content
.
kind: "text" | "markdown" | "readReceipt" | "reaction" | "attachment" | "remoteAttachment" | "multiRemoteAttachment" | "transactionReference" | "walletSendCalls" | "actions" | "intent" | "reply" | "groupUpdated" | "leaveRequest" | "deletedMessage" | "custom" | "unknown"
kind
=== "reply" &&
const message: Message
message
.
Message.content: {
readonly kind: "reply";
readonly referenceId: MessageId;
readonly body: MessageBody;
}
content
.
body: MessageBody
body
.
kind: "text" | "markdown" | "readReceipt" | "attachment" | "remoteAttachment" | "multiRemoteAttachment" | "transactionReference" | "walletSendCalls" | "actions" | "intent" | "groupUpdated" | "leaveRequest" | "deletedMessage" | "custom" | "unknown"
kind
=== "text"
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const message: Message
message
.
Message.content: {
readonly kind: "reply";
readonly referenceId: MessageId;
readonly body: MessageBody;
}
content
.
referenceId: string
referenceId
,
const message: Message
message
.
Message.content: {
readonly kind: "reply";
readonly referenceId: MessageId;
readonly body: MessageBody;
}
content
.
body: {
readonly kind: "text";
readonly value: string;
}
body
.
value: string
value
);
}
if (
const message: Message
message
.
Message.content: MessageContent
content
.
kind: "text" | "markdown" | "readReceipt" | "reaction" | "attachment" | "remoteAttachment" | "multiRemoteAttachment" | "transactionReference" | "walletSendCalls" | "actions" | "intent" | "reply" | "groupUpdated" | "leaveRequest" | "deletedMessage" | "custom" | "unknown"
kind
=== "unknown")
var console: Console
console
.
Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

MDN Reference

error
(
const message: Message
message
.
Message.content: {
readonly kind: "unknown";
readonly encoded?: EncodedContent | undefined;
readonly rawBytes: Uint8Array;
readonly error: ErrorDetails;
}
content
.
error: ErrorDetails
error
);
for (const
const reaction: ReactionMessage
reaction
of
const message: Message
message
.
MessageFields.reactions: ReactionMessage[]
reactions
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const reaction: ReactionMessage
reaction
.
senderInboxId: string
senderInboxId
,
const reaction: ReactionMessage
reaction
.
reaction: Reaction
reaction
.
content: string
content
);
}
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const message: Message
message
.
MessageFields.sentAt: Timestamp
sentAt
.
Timestamp.date: Date
date
,
const message: Message
message
.
MessageFields.sentAt: Timestamp
sentAt
.
Timestamp.ns: bigint
ns
,
const message: Message
message
.
MessageFields.replyCount: bigint
replyCount
);
}
const
const count: bigint
count
= await
group: Group
group
.
Group.countMessages(options: ListMessagesOptions | undefined): Promise<bigint>
countMessages
(
var undefined
undefined
);

New message actions are refresh(), delete(), deleteLocally(), react(...), reply(...), parent(), conversation(), and client(). A message’s actions need its originating client to remain open. await client.decodeContent(encoded) decodes an envelope into the public content union.

Some content records have new fields:

v5.7.0 payloadv8 payload
GroupUpdatedContent.membersAdded, membersRemoved with { inboxId }, and metadataFieldsChangedGroupUpdated.addedInboxes, removedInboxes, and leftInboxes contain inbox ID strings. metadataFieldChanges and the admin and super-admin arrays are new.
Optional DeleteMessageContent.messageIdRequired messageId.
Base64 string attachment datacontent: Uint8Array.
RemoteAttachmentContent with string secret, salt, nonce, and contentLengthRemoteAttachment with Uint8Array keys and a number contentLength. Build it with client.attachments or remoteAttachmentFromEncrypted.

Stream methods return a stream object at once. Iterate it, or register one callback with onValue. await stream.ready() waits for the local reader to open; it does not prove the backend is online. Close a stream with await stream.end(), rather than a cancelStream* method.

v5.7.0 callv8 call
conversations.stream(callback, type, onClose)conversations.stream({ conversationKind?, consentStates?, onClose? }). Omit conversationKind for all conversations.
conversations.streamAllMessages(callback, type, consentStates, onClose)conversations.streamAllMessages({ conversationKind?, consentStates?, from?, onClose? }).
Type filter "all", "groups", "dms"Omit conversationKind, or use "group" or "dm".
group.streamMessages(callback, onClose) returning an unsubscribe functiongroup.streamMessages(options?) or dm.streamMessages(options?), returning a MessageStream.
cancelStream(), cancelStreamAllMessages(), the returned unsubscribe functionawait stream.end().
onClose() with no reasononClose({ kind: "closed" }) or onClose({ kind: "failed", error }). onConnectionStateChange is new.
conversations.streamMessageDeletions(callback) / cancelStreamMessageDeletions()client.events({ kinds: ["message.deleted"] }).
preferences.streamConsent(callback) / cancelStreamConsent()client.events({ kinds: ["consent.changed"] }).
preferences.streamPreferenceUpdates(callback) / cancelStreamPreferenceUpdates()client.events({ kinds: ["hmac_keys.updated"] }).

Before:

Before (xmtp-react-native v5.7.0)
import type { Client, DecodedMessage } from "@xmtp/react-native-sdk";
export async function consume(
client: Client,
process: (message: DecodedMessage) => Promise<void>,
) {
await client.conversations.streamAllMessages(
async (message) => await process(message),
"all",
["allowed"],
() => console.log("closed"),
);
// Later:
client.conversations.cancelStreamAllMessages();
}

After:

const
const messages: MessageStream
messages
=
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.streamAllMessages(options?: MessageStreamOptions): MessageStream
streamAllMessages
({
consentStates?: readonly ConsentState[] | undefined
consentStates
: ["allowed"],
onClose?: ((reason: StreamCloseReason) => void) | undefined
onClose
: (
reason: StreamCloseReason
reason
) => {
if (
reason: StreamCloseReason
reason
.
kind: "closed" | "failed"
kind
=== "failed")
var console: Console
console
.
Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

MDN Reference

error
(
reason: {
readonly kind: "failed";
readonly error: unknown;
}
reason
.
error: unknown
error
);
},
});
await
const messages: MessageStream
messages
.
ReaderStream<Message>.ready(): Promise<void>
ready
();
try {
for await (const
const message: Message
message
of
const messages: MessageStream
messages
) await
process: (message: Message) => Promise<void>
process
(
const message: Message
message
);
} finally {
await
const messages: MessageStream
messages
.
ReaderStream<Message>.end(): Promise<void>
end
();
}

A next-item request acknowledges the previous message. Await all processing before the next request. return(), end(), and cancellation do not acknowledge the last item. Callback mode awaits the callback before it requests the next message. Do not also iterate a callback stream.

const
const messages: MessageStream
messages
=
group: Group
group
.
Group.streamMessages(options?: ConversationMessageStreamOptions): MessageStream
streamMessages
();
await
const messages: MessageStream
messages
.
ReaderStream<Message>.onValue(callback: (value: Message) => void | Promise<void>): Promise<void>

Await each callback before the next read. For messages, that read acknowledges the prior message. Await all processing in the callback. Work sent to an app queue or an unawaited task can continue after acknowledgement.

onValue
(async (
message: Message
message
) => {
await
process: (message: Message) => Promise<void>
process
(
message: Message
message
);
});
// Later, when the owning screen or service stops:
await
const messages: MessageStream
messages
.
ReaderStream<Message>.end(): Promise<void>
end
();

Version 5.7.0 delivered only new traffic. A v8 message reader can deliver retained messages when it opens, then continue with new messages. Make app effects safe to repeat. Only one default message reader owns delivery progress for a database. A second default reader fails with ConsumerOwned. Use options.from for independent replay. For history followed by live delivery, use one snapshot boundary:

const
const history: MessageHistorySnapshot
history
= await
group: Group
group
.
Group.messageHistorySnapshot(limit: number): Promise<MessageHistorySnapshot>
messageHistorySnapshot
(100);
for (const
const message: Message
message
of
const history: MessageHistorySnapshot
history
.
messages: Message[]
messages
) await
process: (message: Message) => Promise<void>
process
(
const message: Message
message
);
const
const live: MessageStream
live
=
group: Group
group
.
Group.streamMessages(options?: ConversationMessageStreamOptions): MessageStream
streamMessages
({
from?: string | undefined
from
:
const history: MessageHistorySnapshot
history
.
cursor: string
cursor
});
for await (const
const message: Message
message
of
const live: MessageStream
live
) await
process: (message: Message) => Promise<void>
process
(
const message: Message
message
);
const
const stream: ConversationStream
stream
=
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.stream(options?: ConversationStreamOptions): ConversationStream
stream
({
conversationKind?: ConversationKind | undefined
conversationKind
: "group" });
for await (const
const conversation: Conversation
conversation
of
const stream: ConversationStream
stream
) await
process: (conversation: Conversation) => Promise<void>
process
(
const conversation: Conversation
conversation
);

A live event subscription reports changes caused by sync, streams, workers, or app calls. It does not fetch messages. await client.startListener(filter, callback) returns a bigint listener ID; await client.stopListener(id) ends it. Handle "lagged" by reading current state again.

const
const events: EventStream
events
= await
client: Client
client
.
Client.events(filter: EventFilter): Promise<EventStream>

A stream of the client events that filter selects.

events
({
kinds: EventKind[]
kinds
: [
"consent.changed",
"message.deleted",
"hmac_keys.updated",
"conversation.metadata_changed",
],
});
for await (const
const event: ClientEvent
event
of
const events: EventStream
events
) await
process: (event: ClientEvent) => Promise<void>
process
(
const event: ClientEvent
event
);

The SDK suspends and resumes stream work with the native app lifecycle. Do not add an AppState listener that restarts SDK streams. See Update ownership and background work.

Section titled “Consent, archives, attachments, and notifications”

Replace the consent entry type and separate value with one entity:

v5.7.0 consent APIv8 consent API
new ConsentRecord(value, "inbox_id" | "conversation_id", state){ entity: { kind: "inbox", inboxId } | { kind: "conversation", conversationId }, state }.
preferences.setConsentState(record)preferences.setConsentStates([record]).
preferences.setConsentStates(records)Same name, with the new record shape.
preferences.inboxIdConsentState(id)preferences.consentState({ kind: "inbox", inboxId }).
preferences.conversationConsentState(id)preferences.consentState({ kind: "conversation", conversationId }), or the conversation’s state snapshot.
preferences.sync(), preferences.syncConsent()preferences.sync().
await
client: Client
client
.
ClientMembers.preferences: Preferences
preferences
.
Preferences.setConsentStates(records: Array<ConsentRecord>): Promise<void>
setConsentStates
([
{
entity: ConsentEntity
entity
: {
kind: "conversation"
kind
: "conversation",
conversationId: string
conversationId
},
state: ConsentState
state
: "allowed" },
{
entity: ConsentEntity
entity
: {
kind: "inbox"
kind
: "inbox",
inboxId: string
inboxId
},
state: ConsentState
state
: "denied" },
]);
const
const state: ConsentState
state
= await
client: Client
client
.
ClientMembers.preferences: Preferences
preferences
.
Preferences.consentState(entity: ConsentEntity): Promise<ConsentState>
consentState
({
kind: "inbox"
kind
: "inbox",
inboxId: string
inboxId
,
});
await
client: Client
client
.
ClientMembers.preferences: Preferences
preferences
.
Preferences.sync(): Promise<void>
sync
();

Local archive operations moved from Client to client.archives:

v5.7.0 archive APIv8 archive API
client.createArchive(path, key, opts?)client.archives.exportToFile(path, key, options): Promise<ArchiveMetadata>. Pass undefined for default options.
client.importArchive(path, key)client.archives.importFromFile(path, key).
client.archiveMetadata(path, key)client.archives.metadataFromFile(path, key).
No equivalentexportToBytes(key, options), importFromBytes(data, key), and metadataFromBytes(data, key).
ArchiveOptions.archiveElements, startNs, endNs, excludeDisappearingMessageselements, start, end, and excludeDisappearingMessages. Times are Timestamp values. Omitted elements selects both kinds.
ArchiveMetadata.archiveVersion, exportedAtNs, startNs, endNsbackupVersion, exportedAt, start, and end.
sendSyncRequest, sendSyncArchive, processSyncArchive, listAvailableArchivesRemoved. No history server transfers archives. syncAllDeviceSyncGroups() remains.
const
const metadata: ArchiveMetadata
metadata
= await
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.exportToFile(path: string, keyBytes: Uint8Array, options: ArchiveOptions | undefined): Promise<ArchiveMetadata>
exportToFile
(
archivePath: string
archivePath
,
key: Uint8Array<ArrayBufferLike>
key
, {
start?: Timestamp | undefined
start
: new
new Timestamp(ns: bigint): Timestamp
Timestamp
(0n),
elements?: ArchiveElement[] | undefined
elements
: ["messages", "consent"],
excludeDisappearingMessages?: boolean | undefined
excludeDisappearingMessages
: true,
});
await
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.importFromFile(path: string, keyBytes: Uint8Array): Promise<void>
importFromFile
(
archivePath: string
archivePath
,
key: Uint8Array<ArrayBufferLike>
key
);
const
const inspected: ArchiveMetadata
inspected
= await
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.metadataFromFile(path: string, keyBytes: Uint8Array): Promise<ArchiveMetadata>
metadataFromFile
(
archivePath: string
archivePath
,
key: Uint8Array<ArrayBufferLike>
key
);
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const metadata: ArchiveMetadata
metadata
.
exportedAt: Timestamp
exportedAt
.
Timestamp.date: Date
date
,
const inspected: ArchiveMetadata
inspected
.
backupVersion: number
backupVersion
);

Pass a path in app-accessible storage, such as the app’s document directory. Archive keys still require exactly 32 bytes. These APIs do not restore active conversations on another backend. Follow the history migration limits.

Version 5.7.0 encrypted a file:// URI and left upload to the app. When the deployment offers attachment storage, v8 stages, uploads, and downloads native files through client.attachments:

v5.7.0 helper or resultv8 helper or result
client.encryptAttachment({ fileUri, mimeType, filename }), then app uploadclient.attachments.create({ kind: "path", path, filename, mimeType }) returns a PendingAttachment. Await upload(), then send its remoteAttachment.
send({ remoteAttachment: { ...metadata, scheme: "https://", url } })sendRemoteAttachment(pending.remoteAttachment).
App download, then client.decryptAttachment({ encryptedLocalFileUri, metadata })client.attachments.download(remote) returns { path, mimeType?, filename? }.
file:// URIsNative file paths.
No equivalentstatus(), pending(remote), listPending(), localPath(remote), listLocal(), and deleteLocal(remote). A failed upload or download needs an explicit retry.

Before:

Before (xmtp-react-native v5.7.0)
import type { Client, Group } from "@xmtp/react-native-sdk";
export async function sendFile(
client: Client,
group: Group,
fileUri: string,
upload: (encryptedFileUri: string) => Promise<string>,
) {
const encrypted = await client.encryptAttachment({
fileUri,
mimeType: "image/jpeg",
filename: "photo.jpg",
});
const url = await upload(encrypted.encryptedLocalFileUri);
await group.send({
remoteAttachment: { ...encrypted.metadata, scheme: "https://", url },
});
}

After:

if (!
client: Client
client
.
ClientMembers.attachments: Attachments
attachments
.
Attachments.offered: boolean
offered
)
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Attachments are unavailable");
const
const pending: PendingAttachment
pending
= await
client: Client
client
.
ClientMembers.attachments: Attachments
attachments
.
Attachments.create(source: AttachmentSource): Promise<PendingAttachment>
create
({
kind: "path"
kind
: "path",
path: string
path
,
filename?: string | undefined
filename
: "photo.jpg",
mimeType: string
mimeType
: "image/jpeg",
});
await
const pending: PendingAttachment
pending
.
PendingAttachment.upload(): Promise<void>
upload
();
await
group: Group
group
.
Group.sendRemoteAttachment(attachment: RemoteAttachment, options?: SendOptions): Promise<MessageId>
sendRemoteAttachment
(
const pending: PendingAttachment
pending
.
PendingAttachment.remoteAttachment: RemoteAttachment
remoteAttachment
);
const
const downloaded: DownloadedAttachment
downloaded
= await
client: Client
client
.
ClientMembers.attachments: Attachments
attachments
.
Attachments.download(remote: RemoteAttachment): Promise<DownloadedAttachment>
download
(
const pending: PendingAttachment
pending
.
PendingAttachment.remoteAttachment: RemoteAttachment
remoteAttachment
,
);
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const downloaded: DownloadedAttachment
downloaded
.
path: string
path
,
const downloaded: DownloadedAttachment
downloaded
.
mimeType?: string | undefined
mimeType
);

If the app keeps its own file host, encrypt the attachment envelope in the app. encryptFile(input, output) and decryptFile(input, output, keys) also work on native file paths.

const
const codec: AttachmentCodec
codec
= new
new AttachmentCodec(): AttachmentCodec
AttachmentCodec
();
const
const envelope: Uint8Array<ArrayBufferLike>
envelope
=
function encodeEncodedContent(content: EncodedContent): Uint8Array
encodeEncodedContent
(
const codec: AttachmentCodec
codec
.
StandardCodec<Attachment, Attachment>.encode(value: Attachment): EncodedContent
encode
({
filename?: string | undefined
filename
: "photo.jpg",
mimeType: string
mimeType
: "image/jpeg",
content: Uint8Array<ArrayBufferLike>
content
:
data: Uint8Array<ArrayBufferLike>
data
,
}),
);
const
const encrypted: EncryptedEncodedContent
encrypted
= await
function encryptEncodedContent(content: Uint8Array): Promise<EncryptedEncodedContent>
encryptEncodedContent
(
const envelope: Uint8Array<ArrayBufferLike>
envelope
);
const
const remote: RemoteAttachment
remote
=
function remoteAttachmentFromEncrypted(url: string, encryptedEncodedContent: EncryptedEncodedContent, filename: string | undefined): RemoteAttachment
remoteAttachmentFromEncrypted
(
url: string
url
,
const encrypted: EncryptedEncodedContent
encrypted
, "photo.jpg");
const
const decrypted: Attachment
decrypted
=
const codec: AttachmentCodec
codec
.
StandardCodec<Attachment, Attachment>.decode(encoded: EncodedContent): Attachment
decode
(
function decodeEncodedContent(bytes: Uint8Array): EncodedContent
decodeEncodedContent
(await
function decryptEncodedContent(encrypted: EncryptedEncodedContent): Promise<Uint8Array>
decryptEncodedContent
(
const encrypted: EncryptedEncodedContent
encrypted
)),
);

Replace the push server, topic subscriptions, and HMAC-key filtering with the SDK’s notification API. The app still obtains OS permission and the APNs or FCM token:

v5.7.0 push APIv8 push API
XMTPPush.register(server, token)client.enableNotifications({ channel: { kind: "apns" | "fcm", token } }). The backend delivers pushes; there is no push server URL.
new XMTPPush(client).subscribe(topics) with getAllPushTopics()Removed. The SDK maintains subscriptions while its task runner runs.
getHmacKeys() for self-push filteringNot needed for registration. conversations.hmacKeys() and conversation.hmacKeys() still return the keys.
No per-conversation controlconversation.setNotifications("enabled" | "disabled" | "default"). Read notificationsEnabled from group .common or DM state.
No equivalentclient.notificationState() and client.disableNotifications().
await
client: Client
client
.
ClientMembers.enableNotifications(config: NotificationConfig): Promise<NotificationState>
enableNotifications
({
channel: NotificationChannel
channel
: {
kind: "apns" | "fcm"
kind
:
platform: "ios" | "android"
platform
=== "ios" ? "apns" : "fcm",
token: string
token
},
});
await
group: Group
group
.
Group.setNotifications(value: NotificationOverride): Promise<void>
setNotifications
("disabled");
const
const enabled: boolean
enabled
= (await
group: Group
group
.
Group.state(): Promise<GroupState>
state
()).
common: ConversationState
common
.
notificationsEnabled: boolean
notificationsEnabled
;
await
group: Group
group
.
Group.setNotifications(value: NotificationOverride): Promise<void>
setNotifications
("default");
const
const state: NotificationState
state
=
client: Client
client
.
ClientMembers.notificationState(): NotificationState
notificationState
();
if (
const state: NotificationState
state
.
kind: "enabled" | "disabled" | "failed"
kind
=== "failed")
var console: Console
console
.
Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

MDN Reference

error
(
const state: {
readonly kind: "failed";
readonly error: NotificationFailure;
}
state
.
error: NotificationFailure
error
);
await
client: Client
client
.
ClientMembers.disableNotifications(): Promise<void>
disableNotifications
();

Call enableNotifications again when the provider token changes. Re-register after migration; old push-server registrations are not imported. Push payloads carry a base64 binary topic and decimal-string sequence_id, with no message bytes. Rewrite native handlers to sync and read local messages. See Push notifications.

Convert closed legacy history to an archive

Section titled “Convert closed legacy history to an archive”

A package update does not open or adopt the old database. Keep a recoverable copy of the legacy database and its key. Close the legacy SDK before conversion. Create a new current database for the target backend and identity, then convert eligible history to an archive and import it:

import {
function prepareMigrationArchive(args: PrepareMigrationArchiveArgs): Promise<MigrationReport>
prepareMigrationArchive
} from "@xmtp/react-native-sdk";
import type {
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
} from "@xmtp/react-native-sdk";
const
const archive: MigrationReport
archive
= await
function prepareMigrationArchive(args: PrepareMigrationArchiveArgs): Promise<MigrationReport>
prepareMigrationArchive
({
databasePath: string
databasePath
,
databaseKey?: Uint8Array<ArrayBufferLike> | undefined
databaseKey
,
archiveKey: Uint8Array<ArrayBufferLike>
archiveKey
,
outputPath: string
outputPath
,
});
await
client: Client
client
.
ClientMembers.archives: Archives
archives
.
Archives.importFromFile(path: string, keyBytes: Uint8Array): Promise<void>
importFromFile
(
const archive: MigrationReport
archive
.
archivePath: string
archivePath
,
archiveKey: Uint8Array<ArrayBufferLike>
archiveKey
);

The native converter accepts legacy databases whose recorded migrations match the checked legacy migration chain, including known prefixes it can advance. The pinned chain comes from libxmtp 6a8e969785c62e0923cf3d8531aff4eaac3c91b7: 64 migrations used by Node 6.1.0 and Browser 7.1.0; Android and iOS 4.10.0 have 62, and 4.11.0 have 64. A React Native package version alone does not prove schema compatibility. Unknown, reordered, or changed migrations fail with MigrationUnsupportedSchema. Retain the original database when conversion fails. Do not describe this as support for every old React Native release.

Conversion works on a copy of a closed source and preserves the source. A source lock produces StorageBusy; close every legacy connection first. The archive contains eligible messages, conversation metadata, and consent. It excludes installation keys, MLS secrets, attachment files, and messages with stored expiry deadlines. Import does not restore the old installation or move its network identity. Arrange account registration and conversation membership on the new backend separately.

Supply a separate 32-byte archive key and an output path in app-accessible storage. Keep the archive key separate from the archive file. Do not overwrite the source with the output.

Only import archives from a trusted source. Import does not verify message provenance or sender membership, and anyone with the key can forge history under another sender. Legacy repeated-nonce archives have weakened confidentiality and authentication; importing them cannot restore those guarantees.

See Backups and the migration overview for conversion errors, archive limits, and restoration behavior.

The React helpers moved to the /react entry and now own a client lifetime.

v5.7.0 helperv8 helper
XmtpProvider, XmtpContext, useXmtp, useClient from the rootThe same names from @xmtp/react-native-sdk/react.
useXmtp() returning { client, setClient }useXmtp() returning { client, error, isLoading, initialize, disconnect }. It throws outside XmtpProvider.
setClient(client)Removed. Pass a caller-owned client as the provider’s client prop.
useClient(onError?) with initialize({ signer, options })Same shape. signer is required, options use the v8 shape, and calls made while initialization is pending share that operation.
disconnect() that only cleared contextdisconnect() cancels pending initialization and releases a client the provider created.
Generic useClient<ContentTypes>()Not generic.

Before:

Before (xmtp-react-native v5.7.0)
import { useClient, type Signer } from "@xmtp/react-native-sdk";
export function useMessaging(signer: Signer, dbEncryptionKey: Uint8Array) {
const { client, initialize, disconnect } = useClient();
return {
client,
connect: () =>
initialize({ signer, options: { env: "production", dbEncryptionKey } }),
disconnect,
};
}

After:

export function
function useMessaging(signer: Signer, backendUrl: string, databaseKey: Uint8Array): {
client: Client | null;
error: Error | null;
isLoading: boolean;
connect: () => Promise<Client>;
disconnect: (this: void) => Promise<void>;
}
useMessaging
(
signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
,
backendUrl: string
backendUrl
: string,
databaseKey: Uint8Array<ArrayBufferLike>
databaseKey
:
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 client: Client | null
client
,
const error: Error | null
error
,
const isLoading: boolean
isLoading
,
const initialize: (this: void, options: InitializeClientOptions) => Promise<Client>
initialize
,
const disconnect: (this: void) => Promise<void>
disconnect
} =
function useClient(onError?: (error: Error) => void): XmtpContextValue
useClient
();
return {
client: Client | null
client
,
error: Error | null
error
,
isLoading: boolean
isLoading
,
connect: () => Promise<Client>
connect
: () =>
const initialize: (this: void, options: InitializeClientOptions) => Promise<Client>
initialize
({
InitializeClientOptions.signer: Signer
signer
,
InitializeClientOptions.options: ClientOptions
options
: {
backend?: BackendSource | undefined
backend
: {
url: string
url
:
backendUrl: string
backendUrl
},
storage: StorageOptions
storage
: {
location: StorageLocation
location
: "default",
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
databaseKey: Uint8Array<ArrayBufferLike>
databaseKey
},
},
}),
disconnect: (this: void) => Promise<void>
disconnect
,
};
}

See Optional React helpers for provider ownership and cancellation rules.

The provider releases clients it creates and preserves caller-owned clients passed through its client prop. Do not close a borrowed client in a hook cleanup unless that component is its actual owner. disconnect() cancels a pending initialization; a late result cannot publish into context. Handle initialization and cleanup errors explicitly.

Use native lifecycle handling supplied by the SDK. A JavaScript AppState listener cannot cover native cold starts or work after runtime teardown. Background push and iOS extensions require native OS integration, bounded sync, and coordinated access to app storage. See Push notifications before shipping the receiver.

Diagnostics, logging, errors, and removals

Section titled “Diagnostics, logging, errors, and removals”
v5.7.0 APIv8 API
client.debugInformation.getNetworkDebugInformation()client.diagnostics.apiStatistics(), identityStatistics(), and aggregateStatistics().
client.debugInformation.clearAllStatistics()client.diagnostics.clearStatistics().
ApiStatistics counters for key packages, group messages, and welcomesApiStats counters publish, query, queryNewest, subscribe, and subscribeStatic, as bigint.
IdentityStatistics.publishIdentityUpdate, getIdentityUpdatesV2Removed. getInboxIds remains. verifySmartContractWalletSignature becomes verifySmartContractWalletSignatures.
Client.activatePersistentLibXMTPLogWriter(level, rotation, maxFiles)enterDebugWriter(directory, rotation, maxFiles, level, processType). The app chooses the directory. Values are strings such as "daily" and "debug".
Client.deactivatePersistentLibXMTPLogWriter()exitDebugWriter().
isLogWriterActive, getXMTPLogFilePaths, readXMTPLogFile, clearXMTPLogs, exportNativeLogsRemoved. Read, share, or delete files in the writer directory from app code.
Numeric LogLevel and LogRotation enumsLogLevel "off", "error", "warn", "info", "debug", "trace"; LogRotation "minutely", "hourly", "daily", "never".
No equivalentinitLogging(options), setLogSink(sink), and flushTelemetry().
Errors as rejected strings or plain Error valuesXmtpError and its subclasses, such as XmtpError.ClientClosed. Inspect details.code, category, retryable, and optional streamFailure. A database written in an older, unsupported format is an XmtpError.Storage whose code is StorageError::PreTransitionDatabase, StorageError::OldStreamDatabase, StorageError::OldEncodingDatabase or StorageError::LegacyGroupDatabase.
No equivalentsuspendStreams() and resumeStreams() act on shared process streams. sdkVersion() returns the library version without a client.
const
const api: ApiStats
api
= await
client: Client
client
.
ClientMembers.diagnostics: Diagnostics
diagnostics
.
Diagnostics.apiStatistics(): Promise<ApiStats>
apiStatistics
();
const
const identity: IdentityStats
identity
= await
client: Client
client
.
ClientMembers.diagnostics: Diagnostics
diagnostics
.
Diagnostics.identityStatistics(): Promise<IdentityStats>
identityStatistics
();
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const api: ApiStats
api
.
publish: bigint
publish
,
const api: ApiStats
api
.
queryNewest: bigint
queryNewest
,
const identity: IdentityStats
identity
.
verifySmartContractWalletSignatures: bigint
verifySmartContractWalletSignatures
,
);
await
client: Client
client
.
ClientMembers.diagnostics: Diagnostics
diagnostics
.
Diagnostics.clearStatistics(): Promise<void>
clearStatistics
();
await
function initLogging(options: LoggingOptions): Promise<void>
initLogging
({
level?: LogLevel | undefined
level
: "info",
structured?: boolean | undefined
structured
: true });
await
function setLogSink(sink?: LogSink): Promise<void>

Install or clear the process log sink. Call initLogging first; before it, both forms fail with the public XmtpError.InvalidInput.

setLogSink
({
async
LogSink.log(record: LogRecord): Promise<void>
log
(
record: LogRecord
record
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
record: LogRecord
record
.
timestamp: Timestamp
timestamp
.
Timestamp.date: Date
date
,
record: LogRecord
record
.
level: LogLevel
level
,
record: LogRecord
record
.
message: string
message
);
},
});
function enterDebugWriter(directory: string, rotation: LogRotation, maxFiles: number, level: LogLevel, processType: LogProcessType): void
enterDebugWriter
(
logDirectory: string
logDirectory
, "daily", 7, "debug", "main");
// Read or upload the files in logDirectory from app code.
function exitDebugWriter(): void
exitDebugWriter
();
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
) {
var console: Console
console
.
Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

MDN Reference

error
(
error: XmtpError
error
.
XmtpError.details: ErrorDetails
details
.
code: string
code
,
error: XmtpError
error
.
XmtpError.details: ErrorDetails
details
.
category: ErrorCategory
category
,
error: XmtpError
error
.
XmtpError.details: ErrorDetails
details
.
retryable: boolean
retryable
,
);
if (
error: XmtpError
error
.
XmtpError.details: ErrorDetails
details
.
streamFailure?: StreamFailureDetails | undefined
streamFailure
)
var console: Console
console
.
Console.error(...data: any[]): void

The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

MDN Reference

error
(
error: XmtpError
error
.
XmtpError.details: ErrorDetails
details
.
streamFailure?: StreamFailureDetails
streamFailure
.
barriers: StreamBarrierFailure[]
barriers
);
}

A PublishedButUnconfirmed failure means the backend accepted the publish but local confirmation did not complete. Sync and inspect the result before you send again. ConfigurationUnavailable, BackendMismatch, ClientVersionTooOld, AuthRequired, ConsumerOwned, and InvalidCursor are other new failures.

End the client after its consumers stop:

await
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
();

For the complete current signatures, use the React Native reference.