Skip to content

Node SDK Upgrade Guide

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

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

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

See Install the XMTP SDK for platform requirements.

Keep the package name @xmtp/node-sdk. SDK 8 requires ESM and Node 22.12 or later. Use import instead of require. Install the recommended version before checking your app. The old @xmtp/node-bindings package is not the public SDK 8 API.

The previous examples below use main at cc878025d. They were checked against that source. The current examples use the local SDK 8 declarations and are checked as complete TypeScript programs.

Previous valueCurrent value
Identifier with identifierKind: IdentifierKind.EthereumPublicIdentity with kind: "ethereum". Use "passkey" for a passkey. The identifier field remains a string.
Numeric enum imports such as ConsentState.Allowed, DeliveryStatus.Published, and SortDirection.DescendingString values such as "allowed", "published", and "descending". Import the type when you need a type annotation.
ConversationType, GroupMessageKind, SortDirection, ListConversationsOrderByTypes ConversationKind, MessageKind, MessageOrder, ConversationOrder. Values use lower camel case, such as "dm", "membershipChange", and "lastActivity".
Nanosecond fields ending in Ns, with a separate Date getterTimestamp. Use .ns for a bigint, .date for a Date, and new Timestamp(ns) for an input. This applies to message, conversation, identity, archive, and action times.
Mutable records and binding classesRead-only public records. IDs remain strings. Byte buffers are Uint8Array. Maps are Map, not plain JavaScript objects.
Generic Client<T>, Group<T>, Dm<T>, Conversation<T>, and DecodedMessage<T>Classes Client, Group, Dm, and Message. Conversation is the type Group | Dm. Narrow a conversation with instanceof Group or instanceof Dm. Narrow received content with content.kind.
Direct new Client(...), client.init(...), or direct conversation/message constructorsUse Client.create, Client.build, and SDK lookup or creation methods. Public SDK objects have private constructors.

A signer has three asynchronous methods: identity(), kind(), and sign(request). Replace type, getIdentifier, and signMessage.

Previous EOA signer:

Before (main cc878025da)
import { IdentifierKind, type Signer } from "@xmtp/node-sdk";
export function createSigner(
address: string,
signText: (text: string) => Promise<Uint8Array>,
): Signer {
return {
type: "EOA",
getIdentifier: () => ({
identifier: address,
identifierKind: IdentifierKind.Ethereum,
}),
signMessage: signText,
};
}

Current EOA signer:

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 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
,
}),
};

Passkey signers are also supported. Their identity uses kind: "passkey"; kind() returns { kind: "passkey" }; sign() returns a "passkey" signature with signature, publicKey, authenticatorData, and clientDataJson byte buffers. Client.generateKeys() and LocalSigningKey.fromBytes(bytes) are new helpers for local Ethereum signers.

Previous client setup:

Before (main cc878025da)
import { Client, type Signer } from "@xmtp/node-sdk";
export async function createClient(signer: Signer, key: Uint8Array) {
return Client.create(signer, {
env: "dev",
appVersion: "chat/7.0.0",
dbPath: "./chat.db3",
dbEncryptionKey: key,
minDbPoolSize: 5,
maxDbPoolSize: 25,
disableDeviceSync: false,
});
}

Current client setup:

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
: {
directory: string
directory
: "./xmtp-data" },
label?: string | undefined
label
: "chat",
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
key: Uint8Array<ArrayBufferLike>
key
,
pool?: StoragePoolOptions | undefined
pool
: {
min?: number | undefined
min
: 5,
max?: number | undefined
max
: 25 },
},
deviceSync?: boolean | undefined
deviceSync
: true,
registration?: RegistrationOptions | undefined
registration
: {
auto?: boolean | undefined
auto
: true },
});
Previous option or helperCurrent option or helper
env, apiUrl, gatewayHost, optional network selectionPass backend: { url } or a Backend. There is no network selected from an environment name. backendUrl is not a Node SDK 8 client option.
Top-level appVersionbackend.appVersion in connection options.
createBackend(options), BackendBuilder, envToString, ApiUrls, XmtpEnvawait Backend.connect({ url, appVersion?, credentials? }). A connected Backend can be shared by clients and standalone queries. Environment helpers are removed.
Old Backend.env, v3Host, gatewayHost, appVersion getters and builder setApiUrl, setGatewayHost, setReadonlyRemoved from the public Backend. Keep connection options in your app and use Backend.connect(options).
dbPath omittedstorage: { location: "default" }. SDK 8 has no implicit dev network label.
dbPath: nullstorage: { location: "inMemory" }.
dbPath: "./chat.db3"storage.location: { dbPath: "./chat.db3", attachmentsDir: "./chat-attachments" }. Both paths are required.
dbPath(inboxId) callbackChoose a directory with storage.location: { directory }, or compute explicit paths in your app before creation. No path callback is accepted.
dbEncryptionKey bytes or hex stringstorage.encryptionKey: Uint8Array. Convert a hex key to bytes in the app. Keep the same 32-byte key when you reopen a database.
minDbPoolSize, maxDbPoolSize, useSingleConnectionstorage.pool: { min, max } and storage.singleConnection.
Environment used in database filenamestorage.label. Keep the label and storage location stable.
disableDeviceSyncdeviceSync: false. Device sync defaults to true.
disableAutoRegisterregistration: { auto: false }. Automatic registration defaults to true. register() remains available.
Top-level nonceregistration.nonce. It is an unsigned 64-bit bigint. New identity generation defaults to nonce 0, rather than the old 1. Keep an explicit nonce where the app requires it.
historySyncUrl, HistorySyncUrlsRemoved. No history server is selected by the client.
waitForRegistrationVisible and VisibilityConfirmationOptionsRemoved from the public client options. Registration waits for the self-hosted backend’s readable state.
WorkerConfigOptions, WorkerIntervalOverride, WorkerJitterOverrideWorkerOptions and WorkerInterval, used by ClientOptions.workers.
workerConfig.defaultIntervalNsworkers.defaultIntervalNs.
workerConfig.workerIntervalsNs, workerJittersNs, disabledWorkersworkers.intervals: [{ kind, intervalNs?, jitterNs?, enabled? }]. Use enabled: false to disable that worker. Worker kinds use strings. New kinds include configurationRefresh, hmacEpoch, and attachmentCleanup.
unstableChangeCallbacks.appData, AppDataChangeObserve "conversation.metadata_changed" through client.events() or startListener(). Events report committed changes; they do not await an app merge before message processing continues.
Top-level logging and OTLP optionsConfigure logging with await initLogging(...); see the diagnostics section below.

Client.create(signer, options) and Client.build(identity, options, inboxId?) require an options object. build reopens a client without storing a signer. Its optional third argument selects the inbox explicitly. SDK 8 can use the inbox stored in explicit storage. allowOffline: true permits startup from stored state when the backend is unavailable. It does not supply the data needed for a first registration.

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
: {
dbPath: string
dbPath
: "./chat.db3",
attachmentsDir: string
attachmentsDir
: "./chat-attachments" },
encryptionKey?: Uint8Array<ArrayBufferLike> | undefined
encryptionKey
:
key: Uint8Array<ArrayBufferLike>
key
,
},
});

The previous 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" },
});

backend.credentials accepts a Credential or a source with asynchronous credential(). A credential has value, signed 64-bit expiresAtSeconds in Unix seconds, and optional header name. Use client.setCredential(credential) to replace a supplied credential. handlers.preAuthenticate accepts a callback with asynchronous run() for work that must precede authentication. forkRecovery selects a policy ("none", "allowlistedGroups", or "all"), group IDs, whether to disable responses, and an optional worker interval. attachments can set maxDownloadBytes, maxPendingAgeSeconds, and allowPrivateNetwork. Client.fetchServerConfiguration(backend) fetches configuration without a client. client.serverConfiguration is a property; await client.refreshServerConfiguration() returns refreshed configuration. Deployment limits and retention counts use bigint.

Previous standalone calls could omit a backend or pass an environment string. Current calls require a BackendSource: { url, appVersion?, credentials? } or a connected Backend.

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
);
Previous callCurrent call and result
client.accountIdentifierclient.identity: PublicIdentity. client.signer and client.env are removed. Keep the signer in your app.
client.isRegisteredawait client.isRegistered(): boolean.
client.preferences.inboxState() / fetchInboxState()await client.inboxState(false) / client.inboxState(true). The refresh flag is required.
client.preferences.getInboxStates(ids) / fetchInboxStates(ids)await client.inboxStates(ids, false) / client.inboxStates(ids, true).
Client.fetchInboxStates(ids, backendOrEnv?, gatewayHost?)Client.inboxStates(ids, backend): Promise<InboxState[]>.
client.fetchInboxIdByIdentifier(identifier)client.inboxIdFor(identity): Promise<string | undefined>.
getInboxIdForIdentifier(backend, identifier)Client.inboxIdFor(identity, backend): Promise<string>. This static method calculates a nonce-0 inbox ID when no network association exists. Use canMessage to test registration.
client.canMessage(identifiers) / Client.canMessage(identifiers, backendOrEnv?)Same method names, with PublicIdentity[] and a required backend for the static method. Both return Map<string, boolean>. Keys are ethereum:<normalized address>, passkey:<lowercase key hex>, or handle:<lowercase handle>, rather than an address alone.
client.fetchLatestInboxUpdatesCount(ids)client.latestInboxUpdatesCount(ids, refreshFromNetwork): Promise<Map<string, bigint>>.
client.fetchOwnInboxUpdatesCount()client.ownInboxUpdatesCount(refreshFromNetwork): Promise<bigint>.
Client.fetchLatestInboxUpdatesCount(ids, backendOrEnv?, gatewayHost?)Free function latestInboxUpdatesCount(ids, backend): Promise<Map<string, bigint>>. There is no static method with the old name.
No old equivalentClient.newestMessageMetadata(ids, backend): Promise<Map<string, MessageMetadataEntry>>. Each entry has sequenceId: bigint and createdAt: Timestamp.
client.fetchKeyPackageStatuses(ids)client.keyPackageStatuses(ids): Promise<Map<string, KeyPackageStatus>>. The old binding result was a plain object. Client.keyPackageStatuses(ids, backend) is new.
client.removeAccount(identity)client.removeAccount(recoverySigner, identity).
client.revokeInstallations(byteIds)client.revokeInstallations(signer, stringIds). Installation IDs are hex strings.
client.revokeAllOtherInstallations()client.revokeAllOtherInstallations(signer).
Client.revokeInstallations(signer, inboxId, byteIds, backendOrEnv?, gatewayHost?)Client.revokeInstallations(signer, inboxId, stringIds, backend).
client.changeRecoveryIdentifier(identity)client.changeRecoveryIdentifier(signer, identity).
Client.isAddressAuthorized(inboxId, address, backendOrEnv?, gatewayHost?)Client.isAddressAuthorized(inboxId, address, backend): Promise<boolean>.
Client.isInstallationAuthorized(inboxId, byteId, backendOrEnv?, gatewayHost?)Client.isInstallationAuthorized(inboxId, stringId, backend): Promise<boolean>.
Synchronous signWithInstallationKey, verifySignedWithInstallationKey, Client.verifySignedWithPublicKeyAwait each method. Signature and public-key inputs remain Uint8Array; verification returns Promise<boolean>.
generateInboxId(identifier, nonce?)generateInboxId(identity, nonce?) remains synchronous. It now takes PublicIdentity, and an omitted nonce is 0.
client.close()await client.end(). End the client before removing storage. client.storage.path() and client.storage.reconnect() are new. await client.storage.delete_() ends the client and removes its database file.

libxmtpVersion, appVersion, options, inboxId, installationId, installationIdBytes, conversations, and preferences remain properties. libxmtpVersion now always returns a string from a created client; appVersion can be absent when connection options do not set it. The options result is always present, has the new option shape, and omits backend credential sources, static credentials, and the storage encryption key. isInMemory and storagePath are new properties.

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

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);
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
);
Previous resultCurrent result
InboxState.identifiers, recoveryIdentifieridentities: PublicIdentity[], recoveryIdentity: PublicIdentity. creationSignatureKind is new and can be absent.
Installation.bytes, clientTimestampNs, idid: string, optional createdAt: Timestamp. There is no bytes field; use the ID text.
LifetimeKeyPackageLifetime. notBefore and notAfter remain Unix-second bigint values. KeyPackageStatus.lifetime and validationError can be absent.
GroupSyncSummary.numEligible, numSynced numberseligible, synced as bigint. This applies to syncAll and syncAllDeviceSyncGroups.

The low-level signature API also changed:

Previous call or typeCurrent call or type
SignatureRequestHandleSignatureRequest, returned by the SDK.
unsafe_createInboxSignatureRequest()unsafeCreateInboxSignatureRequest(). The result can be absent when no signature is needed.
unsafe_addAccountSignatureRequest(identifier, allowInboxReassign = false)unsafeAddAccountSignatureRequest(identity, allowInboxReassign). Supply the boolean explicitly. false checks for an association with another inbox.
unsafe_removeAccountSignatureRequest(identifier)unsafeRemoveAccountSignatureRequest(identity).
unsafe_revokeInstallationsSignatureRequest(byteIds)unsafeRevokeInstallationsSignatureRequest(stringIds).
unsafe_revokeAllOtherInstallationsSignatureRequest()unsafeRevokeAllOtherInstallationsSignatureRequest(). Its result can be absent.
unsafe_changeRecoveryIdentifierSignatureRequest(identifier)unsafeChangeRecoveryIdentifierSignatureRequest(identity).
client.unsafe_addSignature(request, signer?)await request.sign(signer), or await request.addSignature(signature). The signature is a discriminated Signature record.
request.addEcdsaSignature, addScwSignaturerequest.addSignature({ kind: "ecdsa", value: bytes }), or a "scw" signature with bytes, address, chainId, and optional blockNumber.
unsafe_applySignatureRequest(request)unsafeApplySignatureRequest(request).
unsafe_addAccount(newSigner, allowInboxReassign = false)unsafeAddAccount(newSigner, allowInboxReassign). Supply the boolean explicitly.

request.signatureText() remains asynchronous. 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 public method. Do not mix strings and identities in one member list.

Previous creation:

Before (main cc878025da)
import { Client, IdentifierKind } from "@xmtp/node-sdk";
export async function createConversations(client: Client, address: string) {
const peer = { identifier: address, identifierKind: IdentifierKind.Ethereum };
const dm = await client.conversations.createDmWithIdentifier(peer);
const group = await client.conversations.createGroupWithIdentifiers([peer], {
groupName: "Project",
groupDescription: "Project chat",
});
const draft = client.conversations.createGroupOptimistic({
groupName: "Draft",
});
await draft.addMembersByIdentifiers([peer]);
return { dm, group, draft };
}

Current creation:

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
);
Previous methodCurrent method
createGroup(ids, options?), createGroupWithIdentifiers(identities, options?)createGroup(stringIds | identities, options?): Promise<Group>. There is no createGroupWithIdentifiers alias.
createDm(id, options?), createDmWithIdentifier(identity, options?)createDm(id | identity, options?): Promise<Dm>.
Synchronous createGroupOptimistic(options?)Await createGroupOptimistic(options?): Promise<Group>.
getConversationById(id)getById(id): Promise<Conversation | undefined>.
Synchronous getDmByInboxId(id)Await getDmByInboxId(id): Promise<Dm | undefined>.
fetchDmByIdentifier(identity)getDmByIdentity(identity): Promise<Dm | undefined>.
Synchronous getMessageById(id)Await getMessageById(id): Promise<Message | undefined>.
Synchronous listGroups(options?) / listDms(options?)Await listGroups(options | undefined) / listDms(options | undefined). The argument is required; pass undefined for defaults. list, listGroups, and listDms return public conversation objects. The raw ConversationListItem type is removed.
group.addMembersByIdentifiers(identities) / addMembers(ids)group.addMembers(stringIds | identities): Promise<MembershipResult>. The old addedMembers object becomes added: string[]; removedMembers becomes removed: string[]; failedInstallations becomes failedInstallationIds: string[]. Added-member sequence IDs are no longer returned. Check failed installations.
group.removeMembersByIdentifiers(identities) / removeMembers(ids)group.removeMembers(stringIds | identities): Promise<void>.
dm.peerInboxIdawait dm.peerInboxId(): string | null. duplicateDms() still returns Promise<Dm[]>.
conversation.metadata()Read conversation.kind, creatorInboxId, addedByInboxId, and isCreator. Unknown creator or adder IDs are null. addedByInboxId was previously a string.
conversation.createdAt / createdAtNsconversation.createdAt: Timestamp.
conversation.isActive, pausedForVersion(), consentState()For a group, read (await group.state()).common. For a DM, read await dm.state(). Fields are isActive, pausedForVersion, and consentState.
group.name, imageUrl, description, appData, permissions()Read await group.state(). These fields are on GroupState; common state is in .common.
group.isPendingRemoval()(await group.state()).membershipState === "pendingRemove".
Synchronous listAdmins, listSuperAdmins, isAdmin, isSuperAdminAwait these methods. The snapshot also has admins and superAdmins arrays.
members(): Promise<GroupMember[]>members(): Promise<Member[]>. accountIdentifiers becomes identities. installationIds is removed; use client.inboxStates for installation records. inboxId, permissionLevel, and consentState remain.
Synchronous updateConsentState(state) and hmacKeys()Await both. Collection hmacKeys() returns Map<string, HmacKey[]>, rather than a plain object.
lastReadTimes() result with bigint valuesMap<string, Timestamp>.
Cursor type and debugInfo().cursor with { originatorId, sequenceId } recordsbigint[]. epoch stays bigint; the other debug fields remain.
Legacy string topicsconversation.topic is [group_message_v1/<hex-id>]. Collection .topic is removed. Wire topics use a kind byte followed by the identifier; push payloads encode those bytes as base64. See Topics.
processStreamedMessage(envelopeBytes)Removed. A push contains a hint; use conversations.sync(), conversation.sync(), then local message queries.

sync, publishMessages, updateName, updateImageUrl, updateDescription, addAdmin, removeAdmin, addSuperAdmin, removeSuperAdmin, and requestRemoval keep their call forms. syncAll now requires a consent array or undefined: await client.conversations.syncAll(undefined). updateAppData requires its second argument: await group.updateAppData(value, undefined) for an unguarded write. Use string consent and policy values. New methods include peerInboxIds(), membershipCapabilities(), and lastActivityAt(contentTypes); pass undefined to the last method for the default content types.

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

Creation and list options also changed:

Previous fieldCurrent field
groupName, groupImageUrlSquare, groupDescriptionname, imageUrl, description. appData remains.
messageDisappearingSettings: { fromNs, inNs } in group or DM optionsdisappearing: { from: Timestamp, retentionNs: bigint }.
permissions: GroupPermissionsOptions.Default / .AdminOnlypermissions: { kind: "allMembers" } / { kind: "adminOnly" }.
permissions: .CustomPolicy and separate customPermissionPolicySetpermissions: { kind: "custom", policySet }.
PermissionPolicySet.addMemberPolicy, removeMemberPolicy, addAdminPolicy, removeAdminPolicyaddMember, removeMember, addAdmin, removeAdmin.
updateGroupNamePolicy, updateGroupDescriptionPolicy, updateGroupImageUrlSquarePolicy, updateMessageDisappearingPolicy, updateAppDataPolicyupdateName, updateDescription, updateImage, updateDisappearing, updateAppData. Values use "allow", "deny", "admin", or "superAdmin".
GroupPermissions.policyType numeric enum"allMembers", "adminOnly", or "custom". policySet uses the new fields.
updatePermission(PermissionUpdateType, PermissionPolicy, MetadataField?)updatePermission(kind: PermissionUpdateKind, policy: PermissionPolicy, metadataField: MetadataFieldKind | undefined). Supply undefined when no field applies. Use "updateMetadata", for example, and a field such as "name", "description", "imageUrl", "disappearing", or "appData".
List conversationTypekind: "group" | "dm".
List createdAfterNs, createdBeforeNscreatedAfter, createdBefore with Timestamp. New bounds lastActivityAfter and lastActivityBefore also take Timestamp.
orderBy: ListConversationsOrderBy.CreatedAt / .LastActivityorderBy: "createdAt" / "lastActivity".
consentStates, limitSame field names. Consent values use strings. limit is an unsigned 32-bit number.
const
const groups: Group[]
groups
= await
client: Client
client
.
ClientMembers.conversations: Conversations
conversations
.
Conversations.listGroups(options: ListConversationsOptions | undefined): Promise<Array<Group>>
listGroups
({
kind?: ConversationKind | undefined
kind
: "group",
consentStates?: ConsentState[] | undefined
consentStates
: ["allowed"],
createdAfter?: Timestamp | undefined
createdAfter
: new
new Timestamp(ns: bigint): Timestamp
Timestamp
(0n),
includeDuplicateDms?: boolean | undefined
includeDuplicateDms
: false,
});
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 getters and separate update/removal methods:

Previous callCurrent call
messageDisappearingSettings() / isMessageDisappearingEnabled()State fields disappearingSettings / isDisappearingEnabled, in group .common or DM state.
updateMessageDisappearingSettings(fromNs, inNs)updateDisappearingSettings({ from: new Timestamp(fromNs), retentionNs: inNs }).
removeMessageDisappearingSettings()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
);

SDK 8 also exposes typed metadata fields. 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). metadataFieldRef supplies well-known field IDs. This supports app fields and per-inbox values without putting them all in the old opaque appData string. 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
();

Most standard send helpers retain their names. sendText, sendMarkdown, sendReadReceipt, sendAttachment, sendRemoteAttachment, sendMultiRemoteAttachment, sendTransactionReference, sendWalletSendCalls, sendActions, and sendIntent still return a message ID. All use SendOptions.

Previous call or valueCurrent call or value
send(encoded, SendMessageOpts?)send(encoded, SendOptions?), or send(codec, value, SendOptions?).
SendOpts and SendMessageOptsOne SendOptions type: shouldPush?, optimistic?, idempotencyKey?, and new compression?: "deflate" | "gzip".
send(encoded) defaults to shouldPush: false in the old Node wrapperSDK 8 uses the standard content type’s push default. Set { shouldPush: false } explicitly when this is required. A custom codec’s shouldPush(value) can supply its default.
sendReaction({ reference, referenceInboxId, action, content, schema }, options?)sendReaction(reference, referenceInboxId | undefined, { action, content, schema }, options?). Reaction values use strings such as "added" and "unicode".
sendReply({ reference, referenceInboxId?, content }, options?)sendReply(reference, referenceInboxId | undefined, encoded, options?). There is no public type named Reply; use ReplyContent when you need the standard codec payload record.
encodeReaction(reaction) / contentTypeReaction()encodeStandard({ kind: "reaction", reference, referenceInboxId?, reaction }) / standardContentType("reaction"), or ReactionV2Codec.
encodeMarkdown, encodeReadReceipt, encodeAttachment, encodeRemoteAttachment, encodeMultiRemoteAttachment, encodeTransactionReference, encodeWalletSendCalls, encodeActions, encodeIntentUse encodeStandard with the corresponding kind, or a standard codec from @xmtp/node-sdk. encodeText remains.
contentTypeText(), contentTypeReply(), and the other contentType*() functionsstandardContentType("text"), standardContentType("reply"), and the corresponding standard kind.
EncodedContent.type can be absent; parameters is a plain object; envelope has compressiontype is required; parameters is a Map<string, string> and can be omitted when empty. The envelope is uncompressed. Put compression in SendOptions.
Codecs from @xmtp/content-type-primitives, with contentType and decode(encoded, client)SDK ContentCodec<T> with type, encode(value), and decode(encoded). Optional fallback(value) and shouldPush(value) keep the codec’s value type.
NetworkOptions, DeviceSyncOptions, ContentOptions, OtherOptions, BuiltInContentTypes, ExtractCodecContentTypes, DistributiveOmit, EOASigner, SCWSignerRemoved. Use the current ClientOptions, Signer, and MessageContent types.
Client generics infer registered content typesRegister codecs in ClientOptions.codecs for receive decoding. Send with send(codec, typedValue) for compile-time value checks. Received custom values remain unknown until the app checks them.

Previous reaction and reply:

Before (main cc878025da)
import {
Group,
ReactionAction,
ReactionSchema,
encodeText,
} from "@xmtp/node-sdk";
export async function reply(
group: Group,
messageId: string,
senderInboxId: string,
) {
await group.sendReaction({
reference: messageId,
referenceInboxId: senderInboxId,
content: "👍",
action: ReactionAction.Added,
schema: ReactionSchema.Unicode,
});
await group.sendReply({ reference: messageId, content: encodeText("Reply") });
}

Current sends and message actions:

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(encodedOrCodec, ...) stores an unpublished message, queues optimistic publication, and returns its ID. Background processing can publish it before an explicit publish call. Use publishMessage(id) or publishMessages() to drive pending publication. Keep private drafts outside the SDK until the app is ready to send. optimistic and idempotencyKey remain supported.

await client.decodeContent(encoded) is also available to decode an envelope into the public content union.

A custom codec uses public records and needs no client parameter:

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" },
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
);
}

Standard codecs are TextCodec, MarkdownCodec, ReadReceiptCodec, ReactionV2Codec, ReplyCodec, DeleteMessageCodec, AttachmentCodec, RemoteAttachmentCodec, MultiRemoteAttachmentCodec, TransactionReferenceCodec, WalletSendCallsCodec, ActionsCodec, IntentCodec, GroupUpdatedCodec, and LeaveRequestCodec. Text and markdown codecs take strings. Read receipts take void. Reaction, reply, and delete-message codecs take only their matching StandardContent variant. The other codecs take their payload record. Do not install old codec packages to decode built-in content.

Previous query or decoded fieldCurrent query or decoded field
messages(options?): Promise<DecodedMessage[]>, lastMessage()Promise<Message[]>, Promise<Message | undefined>.
countMessages(options?) numbercountMessages(options: ListMessagesOptions | undefined): Promise<bigint>. Pass undefined for defaults.
Query sentBeforeNs, sentAfterNs, insertedBeforeNs, insertedAfterNssentBefore, sentAfter, insertedBefore, insertedAfter, each a Timestamp.
Query enum ContentType[]ContentTypeId[], for example [standardContentType("text")]. This also applies to exclusions. Unknown/custom types cannot be used as these message filters.
Query kind, deliveryStatus, direction, sortBySame names, with string values. excludeSenderInboxIds remains. limit is an unsigned 32-bit number.
message.content is a decoded value or undefinedMessageContent with a kind. Text and markdown use .value; payload records use .value; reactions have .reference, .referenceInboxId, and .reaction; replies have .referenceId and .body.
message.sentAt: Date, sentAtNs, expiresAt, expiresAtNssentAt: Timestamp, optional expiresAt: Timestamp. insertedAt: Timestamp is also available.
message.numReplies numberreplyCount: bigint.
message.reactions: DecodedMessage<Reaction>[]ReactionMessage[], with id, senderInboxId, sentAt, deliveryStatus, and reaction.
EnrichedReply.content, contentType, nested inReplyToReply content.body: MessageBody; message.inReplyTo: ReplyParent, inReplyToContent, and replyContent give the retained parent and bodies. Use await message.parent() for a current Message.
Undefined content after an unknown codec or decode error"custom" content retains encoded, rawBytes, optional decoded value, and optional error. Malformed or undecodable standard content is "unknown" with rawBytes, optional encoded, and ErrorDetails.
Always present decoded contentTypeOptional contentType, fallback, and encoded, plus always present rawBytes.
isText, isMarkdown, isReply, isTextReply, isReaction, isAttachment, isRemoteAttachment, isMultiRemoteAttachment, isTransactionReference, isWalletSendCalls, isActions, isIntent, isReadReceipt, isGroupUpdated, isLeaveRequestRemoved. Check message.content.kind, and for a text reply check message.content.body.kind === "text".

Previous query and content read:

Before (main cc878025da)
import { Group, SortDirection, isText } from "@xmtp/node-sdk";
export async function read(group: Group) {
const messages = await group.messages({
limit: 20,
sentAfterNs: 0n,
direction: SortDirection.Descending,
});
for (const message of messages) {
if (isText(message)) console.log(message.content);
console.log(message.sentAt, message.sentAtNs, message.numReplies);
}
}

Current query and content read:

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
);
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(). Collection methods deleteMessage, deleteMessageLocally, reactToMessage, and replyToMessage also take message IDs. delete publishes a deletion and returns the deletion message ID. deleteLocally only removes the local message. A message’s actions need its originating client to remain open.

Some retained content records have new fields:

Previous payloadCurrent payload
Intent.metadata object with JSON valuesmetadataJson?: string. Serialize with JSON.stringify and parse after validation.
Actions.expiresAtNs, Action.expiresAtNsOptional expiresAt: Timestamp. ActionStyle uses "primary", "secondary", or "danger".
WalletCall.metadata: Record<string, string>WalletCallMetadata with description, transactionType, and extra: Map<string, string>. WalletSendCalls.capabilities is also a Map.
Group-update inbox arrays with { inboxId } objectsThe added, removed, left, admin, and super-admin arrays contain inbox ID strings.
Deleted-message deletedBy enum and separate adminInboxIdOne discriminated deletedBy value: { kind: "sender" } or { kind: "admin", inboxId }.

The old ReadReceipt record is replaced by the "readReceipt" content kind, and the old group-update Inbox record is replaced by inbox ID strings.

Attachment, RemoteAttachment, MultiRemoteAttachment, TransactionReference, TransactionMetadata, and LeaveRequest retain their payload field names. Optional fields use undefined; record fields are read-only.

Use stream methods on client.conversations, a group, or a DM. Each method returns a stream immediately. await stream.ready() waits for the local reader to open. It does not prove that the backend is online or catch-up is complete.

Previous callCurrent call
await conversation.stream(options?)group.streamMessages(options?) or dm.streamMessages(options?).
await conversations.streamAllMessages(options?)client.conversations.streamAllMessages(options?). Options use conversationKind, consentStates, and optional from.
streamAllGroupMessages / streamAllDmMessagesclient.conversations.streamAllMessages( { conversationKind: "group" }) / { conversationKind: "dm" }.
await conversations.stream(options?)client.conversations.stream(options?). Options use conversationKind and consentStates.
streamGroups / streamDmsclient.conversations.stream({ conversationKind: "group" }) / { conversationKind: "dm" }.
StreamOptions with onValue, onEnd, onError, retry controls, and disableSyncCurrent options are signal, onClose, and onConnectionStateChange. Register a callback with await stream.onValue(callback).
onEnd / onErroronClose({ kind: "closed" }) / onClose({ kind: "failed", error }), or handle iterator rejection.
retryAttempts, retryDelay, retryOnFail, onFail, onRetry, onRestart, disableSyncRemoved from the current public stream options. Core handles network recovery. After a terminal failure, repair the cause and explicitly open a replacement stream.
Generic StreamOptions<T, V>, StreamCallback, StreamFunction, StreamValueMutatorA non-generic StreamOptions and callbacks registered on each typed stream. The other helper types are removed.
AsyncStreamProxy, flush(), done()Removed. Streams expose next, return, end, ready, and onValue. Await end() to close a stream.
preferences.streamConsent(options?)client.events({ kinds: ["consent.changed"] }) or startListener with that filter. The result is a typed event, rather than an array of old consent records.
preferences.streamPreferences(options?)Use specific events such as "hmac_keys.updated". There is no general UserPreferenceUpdate[] stream or record.
conversations.streamMessageDeletions / streamDeletedMessagesclient.events({ kinds: ["message.deleted"] }). These are deletion events; they do not supply a full old DecodedMessage.

Previous message consumer:

Before (main cc878025da)
import { Client, type DecodedMessage } from "@xmtp/node-sdk";
export async function consume(
client: Client,
process: (message: DecodedMessage) => Promise<void>,
) {
const stream = await client.conversations.streamAllMessages({
retryAttempts: 10,
});
for await (const message of stream) await process(message);
}

Current message consumer:

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
);
});

Only one default message reader owns delivery progress for a database. This includes readers that select different conversations. A second default reader fails with ConsumerOwned. Use options.from for independent replay. message.deliveryCursor is a string or null; it is local to one database. conversations.beginningDeliveryCursor() selects the first retained message. Save a replay message’s cursor only after processing succeeds.

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

The collection also has messageHistorySnapshot(limit, selection?). The snapshot has messages and cursor. Replay does not change default delivery progress. Current reader scope and filters stay fixed for the lifetime of the reader. There are no groupIds, deliveredCursor, updateScope, updateFilter, catchUpSnapshot, or catchUpChanged members on Node SDK 8’s public stream. Use client.catchUpToLive(timeoutMs) with a millisecond bigint or undefined for a CatchUpSummary with messages, conversations, and failed as bigint, and completed as a boolean.

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. Keep a message stream or explicit sync where the app needs network updates. Event filters use exact event kind strings; group_ids takes binary group IDs and content_types takes event content type records. references_own_messages defaults to false.

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",
"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
);

await client.startListener(filter, callback) returns a bigint listener ID. await client.stopListener(id) ends it. Events use .kind and retain the protocol’s snake_case payload field names. Handle "lagged" by reading current state again; events are not durable message delivery.

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

Replace the old consent entity enum and separate entity text with one value:

Previous consent APICurrent consent API
Consent { entityType, entity, state }ConsentRecord { entity: { kind: "inbox", inboxId } | { kind: "conversation", conversationId }, state }.
getConsentState(ConsentEntityType.InboxId, inboxId)consentState({ kind: "inbox", inboxId }).
Address consent entityRemoved. Resolve the identity to an inbox and set inbox consent.
setConsentStates(records), preferences.sync()Keep the method names; use the new record shape and string states.
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
,
});

Local archive operations moved from Client to client.archives:

Previous archive APICurrent archive API
client.createArchive(path, key, options?)client.archives.exportToFile(path, key, options): Promise<ArchiveMetadata>. The third argument is required; pass undefined for defaults.
client.importArchive(path, key)client.archives.importFromFile(path, key): Promise<void>.
client.archiveMetadata(path, key)client.archives.metadataFromFile(path, key): Promise<ArchiveMetadata>.
Only file archive helpersByte helpers exportToBytes(key, options), importFromBytes(data, key), metadataFromBytes(data, key) are also available on Node. The export options argument is required; pass undefined for defaults.
ArchiveOptions.startNs, endNsstart, end, each an optional Timestamp.
BackupElementSelectionOption valuesArchiveElement strings "messages" and "consent". Omitted elements selects both; [] selects neither. excludeDisappearingMessages remains.
ArchiveMetadata.exportedAtNs, startNs, endNsexportedAt, start, end use Timestamp. backupVersion and elements remain.
sendSyncRequest, sendSyncArchive, processSyncArchive, listAvailableArchives, AvailableArchiveInfoRemoved. No automatic archive transfer through a history server replaces them.

Previous file archive:

Before (main cc878025da)
import { Client, BackupElementSelectionOption } from "@xmtp/node-sdk";
export async function backup(client: Client, key: Uint8Array) {
await client.createArchive("./archive.xmtp", key, {
startNs: 0n,
elements: [
BackupElementSelectionOption.Messages,
BackupElementSelectionOption.Consent,
],
excludeDisappearingMessages: true,
});
await client.importArchive("./archive.xmtp", key);
}

Current file archive:

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
("./archive.xmtp",
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
("./archive.xmtp",
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
(
"./archive.xmtp",
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
);

Archive keys still require exactly 32 bytes. These APIs do not establish that a pre-transition archive can restore active conversations on another backend. Follow the history migration limits.

App-hosted attachment encryption also changed:

Previous helper or resultCurrent helper or result
encryptAttachment(attachment): EncryptedAttachmentEncode the attachment with AttachmentCodec, serialize with encodeEncodedContent, then await encryptEncodedContent(bytes).
EncryptedAttachment.payload, flat secret, salt, nonce, contentDigest, contentLengthEncryptedEncodedContent { ciphertext, keys }. EncryptionKeys has secret, salt, nonce, digest, and length: bigint.
Construct the remote record manuallyremoteAttachmentFromEncrypted(url, encrypted, filename). Supply undefined when there is no filename. This checks the URL and derives the ciphertext digest and length. Upload encrypted.ciphertext before sending the remote record.
decryptAttachment(bytes, remote): AttachmentAwait decryptEncodedContent({ ciphertext: bytes, keys }), parse with decodeEncodedContent, then decode with AttachmentCodec. encryptBytes, decryptBytes, encryptFile, and decryptFile are also available.
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.png",
mimeType: string
mimeType
: "image/png",
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.png");
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
)),
);

When the deployment offers attachment storage, use client.attachments to stage, upload, download, and keep local files. offered is a property. create(source) accepts a "path" or "bytes" source and returns PendingAttachment. Upload it before sending its remoteAttachment property. status() reports waiting, uploading, complete, or failed. A failed upload or download needs an explicit retry by the app. Other helpers are pending(remote), listPending(), localPath(remote), listLocal(), and deleteLocal(remote).

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.png",
mimeType: string
mimeType
: "image/png",
});
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
,
);

Node SDK 8 has new notification APIs. The previous Node SDK did not register push channels through Client. Retire the old push service integration and register with the self-hosted backend:

await
client: Client
client
.
ClientMembers.enableNotifications(config: NotificationConfig): Promise<NotificationState>
enableNotifications
({
channel: NotificationChannel
channel
: {
kind: "fcm"
kind
: "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
();

NotificationChannel selects { kind: "apns", token }, { kind: "fcm", token }, or { kind: "http", url, signingKey }. NotificationConfig also accepts consentStates, includeWelcomes, includeSyncGroups, and includeCommits. enableNotifications returns a NotificationState; notificationState() reads it synchronously. setNotifications("enabled" | "disabled" | "default") sets a conversation override. Read effective notificationsEnabled from group .common or DM state. Call enableNotifications again when the provider token changes.

Push payloads carry a base64 binary topic and decimal-string sequence_id. They contain no encrypted message bytes. Fetch from the backend, process welcomes, sync the conversation, then read local messages. See Push notifications.

Diagnostics, logging, errors, and removals

Section titled “Diagnostics, logging, errors, and removals”
Previous APICurrent API
client.debugInformation.apiStatistics()await client.diagnostics.apiStatistics().
apiIdentityStatistics()await client.diagnostics.identityStatistics().
apiAggregateStatistics()await client.diagnostics.aggregateStatistics().
clearAllStatistics()await client.diagnostics.clearStatistics().
API counters for key packages, group messages, and welcomespublish, query, queryNewest, subscribe, subscribeStatic, all bigint.
IdentityStats.publishIdentityUpdate, getIdentityUpdatesV2Removed. getInboxIds remains. verifySmartContractWalletSignature becomes verifySmartContractWalletSignatures.
structuredLogging, loggingLevel, otelEndpoint, resourceAttributes in client optionsawait initLogging({ structured, level, otel: { endpoint, serviceName, sampleRatio? }, resourceAttributes }). resourceAttributes is a Map.
stdoutLoggingLevelRemoved. No separate client stdout level option exists.
Binding LogOptions, enum LogLevel valuesLoggingOptions, string levels "off", "error", "warn", "info", "debug", "trace". setLogSink({ async log(record) { ... } }) adds an app sink.
Synchronous flushTelemetry()await flushTelemetry().
InboxReassignError, AccountAlreadyAssociatedError, MissingContentTypeError, SignerUnavailableError, ClientNotInitializedError, StreamFailedError, StreamInvalidRetryAttemptsErrorRemoved. Catch XmtpError and its subclasses; inspect details.code, category, retryable, and optional structured failure data.
validHex, isHexString, HexStringRemoved from the SDK. Validate and convert app input in app code.
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
);
},
});
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
);
}

New configuration, storage, and delivery failures are subclasses such as XmtpError.ConfigurationUnavailable, ConfigurationInvalid, BackendMismatch, ClientVersionTooOld, AuthRequired, ChainNotAccepted, ClientClosed, ConsumerOwned, and InvalidCursor. Stream failure details are in error.details.streamFailure, not a getStreamFailureDetails helper. A PublishedButUnconfirmed failure means the publish was accepted but local confirmation did not complete. Sync and inspect the result before you decide to send again.

New native helpers await suspendStreams() and await resumeStreams() act on shared process streams and preserve their durable progress. sdkVersion() returns the library version without a client. For local file logs, enterDebugWriter(directory, rotation, maxFiles, level, processType) and exitDebugWriter() are synchronous; rotation uses "minutely", "hourly", "daily", or "never", and process type uses "main" or "extension".

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 Node reference.